@@ -28,6 +28,9 @@ internal class BaseGlyphBuilder : IGlyphRenderer
2828 private int graphemePathCount ;
2929 private int currentGraphemeIndex = - 1 ;
3030 private readonly List < GlyphPathCollection > currentGlyphs = [ ] ;
31+ private TextDecorationDetails ? previousUnderlineTextDecoration ;
32+ private TextDecorationDetails ? previousOverlineTextDecoration ;
33+ private TextDecorationDetails ? previousStrikeoutTextDecoration ;
3134
3235 // Per-layer (within current grapheme) bookkeeping:
3336 private int layerStartIndex ;
@@ -67,6 +70,9 @@ void IGlyphRenderer.EndText()
6770 this . graphemeBuilder = null ;
6871 this . graphemePathCount = 0 ;
6972 this . currentGraphemeIndex = - 1 ;
73+ this . previousUnderlineTextDecoration = null ;
74+ this . previousOverlineTextDecoration = null ;
75+ this . previousStrikeoutTextDecoration = null ;
7076
7177 this . EndText ( ) ;
7278 }
@@ -248,6 +254,56 @@ void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start
248254 start = ClampToPixel ( start , ( int ) thickness , rotated ) ;
249255 end = ClampToPixel ( end , ( int ) thickness , rotated ) ;
250256
257+ // Sometimes the start and end points do not align properly leaving pixel sized gaps
258+ // so we need to adjust them. Use any previous decoration to try and continue the line.
259+ TextDecorationDetails ? previous = textDecorations switch
260+ {
261+ TextDecorations . Underline => this . previousUnderlineTextDecoration ,
262+ TextDecorations . Overline => this . previousOverlineTextDecoration ,
263+ TextDecorations . Strikeout => this . previousStrikeoutTextDecoration ,
264+ _ => null
265+ } ;
266+
267+ if ( previous != null )
268+ {
269+ // Align the new line with the previous one if they are close enough.
270+ if ( rotated )
271+ {
272+ if ( thickness == previous . Value . Thickness
273+ && previous . Value . End . Y + 1 >= start . Y
274+ && previous . Value . End . X == start . X )
275+ {
276+ start = previous . Value . End ;
277+ }
278+ }
279+ else if ( thickness == previous . Value . Thickness
280+ && previous . Value . End . Y == start . Y
281+ && previous . Value . End . X + 1 >= start . X )
282+ {
283+ start = previous . Value . End ;
284+ }
285+ }
286+
287+ TextDecorationDetails current = new ( )
288+ {
289+ Start = start ,
290+ End = end ,
291+ Thickness = thickness
292+ } ;
293+
294+ switch ( textDecorations )
295+ {
296+ case TextDecorations . Underline :
297+ this . previousUnderlineTextDecoration = current ;
298+ break ;
299+ case TextDecorations . Strikeout :
300+ this . previousStrikeoutTextDecoration = current ;
301+ break ;
302+ case TextDecorations . Overline :
303+ this . previousOverlineTextDecoration = current ;
304+ break ;
305+ }
306+
251307 Vector2 a = start - pad ;
252308 Vector2 b = start + pad ;
253309 Vector2 c = end + pad ;
@@ -343,16 +399,27 @@ public virtual void SetDecoration(TextDecorations textDecorations, Vector2 start
343399 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
344400 private static PointF ClampToPixel ( PointF point , int thickness , bool rotated )
345401 {
402+ // Even. Clamp to whole pixels.
346403 if ( ( thickness & 1 ) == 0 )
347404 {
348405 return Point . Truncate ( point ) ;
349406 }
350407
408+ // Odd. Clamp to half pixels.
351409 if ( rotated )
352410 {
353411 return Point . Truncate ( point ) + new Vector2 ( .5F , 0 ) ;
354412 }
355413
356414 return Point . Truncate ( point ) + new Vector2 ( 0 , .5F ) ;
357415 }
416+
417+ private struct TextDecorationDetails
418+ {
419+ public Vector2 Start { get ; set ; }
420+
421+ public Vector2 End { get ; set ; }
422+
423+ public float Thickness { get ; internal set ; }
424+ }
358425}
0 commit comments