diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 0a7328125a..0ed73e7786 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -1,5 +1,9 @@ -using Avalonia.Collections; +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Collections.Pooled; using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -196,19 +200,66 @@ namespace Avalonia.Media break; } + var strokeOffset = 0.0; + switch (StrokeOffsetUnit) { case TextDecorationUnit.FontRenderingEmSize: - origin += new Point(0, StrokeOffset * textMetrics.FontRenderingEmSize); + strokeOffset = StrokeOffset * textMetrics.FontRenderingEmSize; break; case TextDecorationUnit.Pixel: - origin += new Point(0, StrokeOffset); + strokeOffset = StrokeOffset; break; } + origin += new Point(0, strokeOffset); + var pen = new Pen(Stroke ?? defaultBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); + if (Location == TextDecorationLocation.Underline && MathUtilities.IsZero(strokeOffset)) + { + var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d + strokeOffset), (float)(thickness * 1.5d + strokeOffset)); + + if (intersections != null && intersections.Count > 0) + { + var last = baselineOrigin.X; + var finalPos = last + glyphRun.Size.Width; + var end = last; + + var points = new List(); + + //math is taken from chrome's source code. + for (var i = 0; i < intersections.Count; i += 2) + { + var start = intersections[i] - thickness; + end = intersections[i + 1] + thickness; + if (start > last && last + textMetrics.FontRenderingEmSize / 12 < start) + { + points.Add(last); + points.Add(start); + } + last = end; + } + + if (end < finalPos) + { + points.Add(end); + points.Add(finalPos); + } + + for (var i = 0; i < points.Count; i += 2) + { + var a = new Point(points[i], origin.Y); + var b = new Point(points[i + 1], origin.Y); + + drawingContext.DrawLine(pen, a, b); + } + + return; + } + } + drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); } } diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 7801bdd50f..6a8ae4d954 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Metadata; namespace Avalonia.Platform @@ -7,5 +8,8 @@ namespace Avalonia.Platform /// Actual implementation of a glyph run that stores platform dependent resources. /// [Unstable] - public interface IGlyphRunImpl : IDisposable { } + public interface IGlyphRunImpl : IDisposable + { + IReadOnlyList GetIntersections(float lowerBound, float upperBound); + } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9bb4b34976..cb79cc85db 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -128,6 +128,11 @@ namespace Avalonia.Headless public void Dispose() { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + throw new NotImplementedException(); + } } class HeadlessGeometryStub : IGeometryImpl diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 484fd9f219..dd7ed31a6e 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Metadata; using Avalonia.Platform; using SkiaSharp; @@ -20,6 +21,9 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } + public IReadOnlyList GetIntersections(float upperBound, float lowerBound) => + TextBlob.GetIntercepts(lowerBound, upperBound); + void IDisposable.Dispose() { TextBlob.Dispose(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 0b06d5ef3e..638aaf08d6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.Direct2D1.Media { @@ -15,5 +16,10 @@ namespace Avalonia.Direct2D1.Media { GlyphRun?.Dispose(); } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; + } } } diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 24948aff01..f525e4736b 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.UnitTests { @@ -8,5 +9,10 @@ namespace Avalonia.UnitTests { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; + } } }