diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 09f86f462c..6e534bbb2a 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -273,7 +273,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Constraint = constraint, - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, @@ -490,7 +490,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index dd33023e38..89f672deaa 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -97,9 +97,7 @@ namespace Avalonia.Controls.Primitives { var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1]; - var offsetX = lastLine.LineMetrics.BaselineOrigin.X; - - var lineX = offsetX + lastLine.LineMetrics.Size.Width; + var lineX = lastLine.LineMetrics.Size.Width; var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height; @@ -117,7 +115,7 @@ namespace Avalonia.Controls.Primitives continue; } - var currentX = textLine.LineMetrics.BaselineOrigin.X; + var currentX = 0.0; foreach (var textRun in textLine.TextRuns) { diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 2361ea9011..7e5287f81f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -411,9 +411,30 @@ namespace Avalonia.Controls context.FillRectangle(background, new Rect(Bounds.Size)); } + if (TextLayout is null) + { + return; + } + + var textAlignment = TextAlignment; + + var width = Bounds.Size.Width; + + var offsetX = 0.0; + + switch (textAlignment) + { + case TextAlignment.Center: + offsetX = (width - TextLayout.Size.Width) / 2; + break; + case TextAlignment.Right: + offsetX = width - TextLayout.Size.Width; + break; + } + var padding = Padding; - TextLayout?.Draw(context, new Point(padding.Left, padding.Top)); + TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top)); } /// @@ -431,7 +452,7 @@ namespace Avalonia.Controls return new TextLayout( text ?? string.Empty, - FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), + FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), FontSize, Foreground, TextAlignment, @@ -470,12 +491,12 @@ namespace Avalonia.Controls if (_constraint != availableSize) { + _constraint = availableSize; + InvalidateTextLayout(); } - _constraint = availableSize; - - var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty; + var measuredSize = TextLayout?.Size ?? Size.Empty; return measuredSize.Inflate(padding); } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index bc17fb3faa..763d192693 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -54,7 +54,7 @@ namespace Avalonia.Headless class HeadlessCursorFactoryStub : IStandardCursorFactory { - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); @@ -101,7 +101,7 @@ namespace Avalonia.Headless public bool IsFixedPitch => true; public void Dispose() - { + { } public ushort GetGlyph(uint codepoint) @@ -155,9 +155,9 @@ namespace Avalonia.Headless return new List { "Arial" }; } - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - fontKey = new FontKey("Arial", fontWeight, fontStyle); + fontKey = new FontKey("Arial", fontStyle, fontWeight); return true; } } @@ -169,7 +169,7 @@ namespace Avalonia.Headless { public void Save(Stream outputStream) { - + } } public IWindowIconImpl LoadIcon(string fileName) diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index bc979c15ee..ad3fee7eb7 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -79,12 +79,13 @@ namespace Avalonia.Media /// Returns a new typeface, or an existing one if a matching typeface exists. /// /// The font family. - /// The font weight. /// The font style. + /// The font weight. /// /// The typeface. /// - public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal) + public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal, + FontWeight fontWeight = FontWeight.Normal) { while (true) { @@ -93,7 +94,7 @@ namespace Avalonia.Media fontFamily = _defaultFontFamily; } - var key = new FontKey(fontFamily.Name, fontWeight, fontStyle); + var key = new FontKey(fontFamily.Name, fontStyle, fontWeight); if (_typefaceCache.TryGetValue(key, out var typeface)) { @@ -121,15 +122,16 @@ namespace Avalonia.Media /// Returns null if no fallback was found. /// /// The codepoint to match against. - /// The font weight. /// The font style. + /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. /// /// The matched typeface. /// - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + public Typeface MatchCharacter(int codepoint, FontStyle fontStyle = FontStyle.Normal, + FontWeight fontWeight = FontWeight.Normal, FontFamily fontFamily = null, CultureInfo culture = null) { foreach (var cachedTypeface in _typefaceCache.Values) @@ -142,7 +144,7 @@ namespace Avalonia.Media } } - var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ? _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : null; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs index a8d81648ba..b330db8462 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -4,7 +4,7 @@ namespace Avalonia.Media.Fonts { public readonly struct FontKey : IEquatable { - public FontKey(string familyName, FontWeight weight, FontStyle style) + public FontKey(string familyName, FontStyle style, FontWeight weight) { FamilyName = familyName; Style = style; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 2e7e7aceb1..b71fe5bc3c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -89,16 +89,7 @@ namespace Avalonia.Media.TextFormatting /// The split result. public SplitTextCharactersResult Split(int length) { - var glyphCount = 0; - - var firstCharacters = GlyphRun.Characters.Take(length); - - var codepointEnumerator = new CodepointEnumerator(firstCharacters); - - while (codepointEnumerator.MoveNext()) - { - glyphCount++; - } + var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length); if (GlyphRun.Characters.Length == length) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index b35882fc0e..47e716982c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -71,7 +71,7 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultTypeface.FontFamily); + FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily); if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 793707d0b2..3ad23f3504 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.Media.TextFormatting { foreach (var shapedCharacters in previousLineBreak.RemainingCharacters) { + if (shapedCharacters == null) + { + continue; + } + textRuns.Add(shapedCharacters); if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) @@ -106,7 +111,7 @@ namespace Avalonia.Media.TextFormatting var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface, run.Properties.FontRenderingEmSize, run.Properties.CultureInfo); - var shapedCharacters = new ShapedTextCharacters(glyphRun, textRun.Properties); + var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties); textRuns.Add(shapedCharacters); } @@ -355,9 +360,67 @@ namespace Avalonia.Media.TextFormatting { var glyphRun = textCharacters.GlyphRun; - var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); + if (glyphRun.Bounds.Width < availableWidth) + { + return glyphRun.Characters.Length; + } + + var glyphCount = 0; + + var currentWidth = 0.0; + + if (glyphRun.GlyphAdvances.IsEmpty) + { + var glyphTypeface = glyphRun.GlyphTypeface; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; + } + } + else + { + for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++) + { + var advance = glyphRun.GlyphAdvances[i]; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; + } + } + + if (glyphCount == glyphRun.GlyphIndices.Length) + { + return glyphRun.Characters.Length; + } + + if (glyphRun.GlyphClusters.IsEmpty) + { + return glyphCount; + } + + var firstCluster = glyphRun.GlyphClusters[0]; + + var lastCluster = glyphRun.GlyphClusters[glyphCount]; - return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textCharacters.Text.Start; + return lastCluster - firstCluster; } /// diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 2e2e4a8c68..54745144c8 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -103,12 +103,12 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList TextLines { get; private set; } /// - /// Gets the bounds of the layout. + /// Gets the size of the layout. /// /// /// The bounds. /// - public Rect Bounds { get; private set; } + public Size Size { get; private set; } /// /// Draws the text layout. @@ -126,7 +126,10 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in TextLines) { - textLine.Draw(context, new Point(origin.X, currentY)); + var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, + _paragraphProperties.TextAlignment); + + textLine.Draw(context, new Point(origin.X + offsetX, currentY)); currentY += textLine.LineMetrics.Size.Height; } @@ -158,22 +161,16 @@ namespace Avalonia.Media.TextFormatting /// Updates the current bounds. /// /// The text line. - /// The left. - /// The right. - /// The bottom. - private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom) + /// The current width. + /// The current height. + private static void UpdateBounds(TextLine textLine, ref double width, ref double height) { - if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width) - { - right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width; - } - - if (left < textLine.LineMetrics.BaselineOrigin.X) + if (width < textLine.LineMetrics.Size.Width) { - left = textLine.LineMetrics.BaselineOrigin.X; + width = textLine.LineMetrics.Size.Width; } - bottom += textLine.LineMetrics.Size.Height; + height += textLine.LineMetrics.Size.Height; } /// @@ -204,13 +201,13 @@ namespace Avalonia.Media.TextFormatting TextLines = new List { textLine }; - Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height); + Size = new Size(0, textLine.LineMetrics.Size.Height); } else { var textLines = new List(); - double left = 0.0, right = 0.0, bottom = 0.0; + double width = 0.0, height = 0.0; var currentPosition = 0; @@ -228,9 +225,9 @@ namespace Avalonia.Media.TextFormatting textLines.Add(textLine); - UpdateBounds(textLine, ref left, ref right, ref bottom); + UpdateBounds(textLine, ref width, ref height); - if (!double.IsPositiveInfinity(MaxHeight) && bottom > MaxHeight) + if (!double.IsPositiveInfinity(MaxHeight) && height > MaxHeight) { break; } @@ -247,7 +244,7 @@ namespace Avalonia.Media.TextFormatting textLines.Add(emptyTextLine); } - Bounds = new Rect(left, 0, right, bottom); + Size = new Size(width, height); TextLines = textLines; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index cf00399b8a..a1a9b50793 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -33,8 +33,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, - origin.Y + LineMetrics.BaselineOrigin.Y); + var baselineOrigin = new Point(currentX, origin.Y + LineMetrics.TextBaseline); textRun.Draw(drawingContext, baselineOrigin); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index d47cc0c394..2f7809ff35 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -9,10 +9,10 @@ namespace Avalonia.Media.TextFormatting /// public readonly struct TextLineMetrics { - public TextLineMetrics(Size size, Point baselineOrigin, TextRange textRange) + public TextLineMetrics(Size size, double textBaseline, TextRange textRange) { Size = size; - BaselineOrigin = baselineOrigin; + TextBaseline = textBaseline; TextRange = textRange; } @@ -33,12 +33,9 @@ namespace Avalonia.Media.TextFormatting public Size Size { get; } /// - /// Gets the baseline origin. + /// Gets the distance from the top to the baseline of the line of text. /// - /// - /// The baseline origin. - /// - public Point BaselineOrigin { get; } + public double TextBaseline { get; } /// /// Creates the text line metrics. @@ -81,16 +78,12 @@ namespace Avalonia.Media.TextFormatting } } - var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, paragraphProperties.TextAlignment); - - var baselineOrigin = new Point(xOrigin, -ascent); - var size = new Size(lineWidth, double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ? descent - ascent + lineGap : paragraphProperties.LineHeight); - return new TextLineMetrics(size, baselineOrigin, textRange); + return new TextLineMetrics(size, -ascent, textRange); } } } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 3d0bea8c80..59b08aae0a 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -22,15 +22,16 @@ namespace Avalonia.Platform /// Tries to match a specified character to a typeface that supports specified font properties. /// /// The codepoint to match against. - /// The font weight. /// The font style. + /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching font key. /// /// True, if the could match the character to specified parameters, False otherwise. /// - bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); /// diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 415a89e1c1..91bc937475 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -29,7 +29,8 @@ namespace Avalonia.Skia [ThreadStatic] private static string[] t_languageTagBuffer; - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { SKFontStyle skFontStyle; @@ -80,7 +81,7 @@ namespace Avalonia.Skia continue; } - fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); return true; } @@ -91,7 +92,7 @@ namespace Avalonia.Skia if (skTypeface != null) { - fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); return true; } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 0fdea5ed40..f59a0a32c2 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Platform; +using JetBrains.Annotations; using SkiaSharp; namespace Avalonia.Skia @@ -7,9 +8,9 @@ namespace Avalonia.Skia /// public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKTextBlob textBlob) + public GlyphRunImpl([NotNull] SKTextBlob textBlob) { - TextBlob = textBlob; + TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); } /// diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 7aea90e61e..6c2ac17923 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -19,42 +19,49 @@ namespace Avalonia.Skia public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style); + var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight); return GetNearestMatch(_typefaces, key); } private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) { - if (typefaces.ContainsKey(key)) + if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface)) { - return typefaces[key]; + return typeface; } - var keys = typefaces.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); + var weight = (int)key.Weight; - if (!keys.Any()) - { - keys = typefaces.Keys.Where( - x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); + weight -= weight % 100; // make sure we start at a full weight - if (!keys.Any()) + for (var i = (int)key.Style; i < 2; i++) + { + // only try 2 font weights in each direction + for (var j = 0; j < 200; j += 100) { - keys = typefaces.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && - (x.Style >= key.Style || x.Style < key.Style)).ToArray(); - } - } + if (weight - j >= 100) + { + if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) + { + return typeface; + } + } - if (keys.Length == 0) - { - return null; - } + if (weight + j > 900) + { + continue; + } - key = keys[0]; + if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + { + return typeface; + } + } + } - return typefaces[key]; + //Nothing was found so we use the first typeface we can get. + return typefaces.Values.FirstOrDefault(); } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index a9aed80a04..d36baf331d 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -43,18 +43,20 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - if (assetStream == null) throw new InvalidOperationException("Asset could not be loaded."); + if (assetStream == null) + throw new InvalidOperationException("Asset could not be loaded."); var typeface = SKTypeface.FromStream(assetStream); - if(typeface == null) throw new InvalidOperationException("Typeface could not be loaded."); + if (typeface == null) + throw new InvalidOperationException("Typeface could not be loaded."); if (typeface.FamilyName != fontFamily.Name) { continue; } - var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); + var key = new FontKey(fontFamily.Name, (FontStyle)typeface.FontSlant, (FontWeight)typeface.FontWeight); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 786af7726c..ffe1175567 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -15,51 +15,7 @@ namespace Avalonia.Skia { using (var buffer = new Buffer()) { - buffer.ContentType = ContentType.Unicode; - - var breakCharPosition = text.Length - 1; - - var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); - - if (codepoint.IsBreakChar) - { - var breakCharCount = 1; - - if (text.Length > 1) - { - var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); - - if (codepoint == '\r' && previousCodepoint == '\n' - || codepoint == '\n' && previousCodepoint == '\r') - { - breakCharCount = 2; - } - } - - if (breakCharPosition != text.Start) - { - buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); - } - - var cluster = buffer.GlyphInfos.Length > 0 ? - buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : - (uint)text.Start; - - switch (breakCharCount) - { - case 1: - buffer.Add('\u200C', cluster); - break; - case 2: - buffer.Add('\u200C', cluster); - buffer.Add('\u200D', cluster); - break; - } - } - else - { - buffer.AddUtf16(text.Buffer.Span); - } + FillBuffer(buffer, text); buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); @@ -93,7 +49,7 @@ namespace Avalonia.Skia { glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; - clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster); + clusters[i] = (ushort)glyphInfos[i].Cluster; if (!glyphTypeface.IsFixedPitch) { @@ -112,6 +68,51 @@ namespace Avalonia.Skia } } + private static void FillBuffer(Buffer buffer, ReadOnlySlice text) + { + buffer.ContentType = ContentType.Unicode; + + var i = 0; + + while (i < text.Length) + { + var codepoint = Codepoint.ReadAt(text, i, out var count); + + var cluster = (uint)(text.Start + i); + + if (codepoint.IsBreakChar) + { + if (i < text.End) + { + var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); + + if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r') + { + count++; + + buffer.Add('\u200C', cluster); + + buffer.Add('\u200D', cluster); + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add(codepoint, cluster); + } + + i += count; + } + } + private static void SetOffset(ReadOnlySpan glyphPositions, int index, double textScale, ref Vector[] offsetBuffer) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 253a373106..33af15076d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -32,7 +32,8 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -50,7 +51,7 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle); + fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight); return true; } diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index e65cdf0312..8683da9a01 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle), + FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index feed1179ef..f36d6d9e4a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -38,7 +38,7 @@ namespace Avalonia.Skia.UnitTests.Media private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { foreach (var customTypeface in _customTypefaces) @@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media continue; } - fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle); + fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight); return true; } @@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle); + fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); return true; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 697cc4fec7..4a88b259bc 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -260,6 +260,39 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Not_Produce_TextLine_Wider_Than_ParagraphWidth() + { + using (Start()) + { + const string text = + "Multiline TextBlock with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. " + + "Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. " + + "Vivamus pretium ornare est."; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textSourceIndex = 0; + + while (textSourceIndex < text.Length) + { + var textLine = + formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties); + + Assert.True(textLine.LineMetrics.Size.Width <= 200); + + textSourceIndex += textLine.TextRange.Length; + } + } + } + public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 5d9aa2cf97..43a791b2cb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -512,7 +512,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(numberOfLines, layout.TextLines.Count); - Assert.Equal(numberOfLines * lineHeight, layout.Bounds.Height); + Assert.Equal(numberOfLines * lineHeight, layout.Size.Height); } } diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index 55656fcfc0..e614c60310 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -25,7 +25,7 @@ namespace Avalonia.UnitTests return new[] { _defaultFamilyName }; } - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { fontKey = default; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index e219682fa6..42e573c8a5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -34,6 +34,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Background = Brushes.Red, Child = textBlock = new TextBlock { + TextWrapping = TextWrapping.NoWrap, Text = "Hello World", } } diff --git a/tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs b/tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs new file mode 100644 index 0000000000..1afd84e546 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Utilities +{ + public class ReadOnlySpanTests + { + [Fact] + public void Should_Skip() + { + var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var slice = new ReadOnlySlice(buffer); + + var skipped = slice.Skip(2); + + var expected = buffer.Skip(2); + + Assert.Equal(expected, skipped); + } + + [Fact] + public void Should_Take() + { + var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var slice = new ReadOnlySlice(buffer); + + var taken = slice.Take(8); + + var expected = buffer.Take(8); + + Assert.Equal(expected, taken); + } + } +}