@@ -31,9 +31,6 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa
3131 private TextRun ? currentTextRun ;
3232 private Brush ? currentBrush ;
3333 private Pen ? currentPen ;
34- private TextDecorationDetails ? currentUnderline ;
35- private TextDecorationDetails ? currentStrikeout ;
36- private TextDecorationDetails ? currentOverline ;
3734 private bool currentDecorationIsVertical ;
3835 private bool hasLayer ;
3936
@@ -166,20 +163,9 @@ protected override void EndLayer()
166163 this . currentPen = this . defaultPen ;
167164 }
168165
169- bool renderFill = false ;
170- bool renderOutline = false ;
171-
172- // If we are using the fonts color layers we ignore the request to draw an outline only
173- // because that won't really work. Instead we force drawing using fill with the requested color.
174- if ( this . currentBrush != null )
175- {
176- renderFill = true ;
177- }
178-
179- if ( this . currentPen != null )
180- {
181- // renderOutline = true;
182- }
166+ // When rendering layers we only fill them.
167+ // Any drawing of outlines is ignored as that doesn't really make sense.
168+ bool renderFill = this . currentBrush != null ;
183169
184170 // Path has already been added to the collection via the base class.
185171 IPath path = this . CurrentPaths . Last ( ) ;
@@ -200,12 +186,6 @@ protected override void EndLayer()
200186 // We can use this to offset the render location on the next instance of this glyph.
201187 renderData . LocationDelta = ( Vector2 ) ( path . Bounds . Location - renderLocation ) ;
202188
203- if ( renderOutline )
204- {
205- path = this . currentPen ! . GeneratePath ( path ) ;
206- renderData . OutlineMap = this . Render ( path ) ;
207- }
208-
209189 if ( ! this . noCache )
210190 {
211191 this . UpdateCache ( renderData ) ;
@@ -254,18 +234,6 @@ protected override void EndLayer()
254234 RenderPass = RenderOrderFill
255235 } ) ;
256236 }
257-
258- if ( renderData . OutlineMap != null )
259- {
260- int offset = ( int ) ( ( this . currentPen ? . StrokeWidth ?? 0 ) / 2 ) ;
261- this . DrawingOperations . Add ( new DrawingOperation
262- {
263- RenderLocation = renderLocation - new Size ( offset , offset ) ,
264- Map = renderData . OutlineMap ,
265- Brush = this . currentPen ? . StrokeFill ?? this . currentBrush ! ,
266- RenderPass = RenderOrderOutline
267- } ) ;
268- }
269237 }
270238
271239 public override TextDecorations EnabledDecorations ( )
@@ -301,24 +269,6 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star
301269 return ;
302270 }
303271
304- ref TextDecorationDetails ? targetDecoration = ref this . currentStrikeout ;
305- if ( textDecorations == TextDecorations . Strikeout )
306- {
307- targetDecoration = ref this . currentStrikeout ;
308- }
309- else if ( textDecorations == TextDecorations . Underline )
310- {
311- targetDecoration = ref this . currentUnderline ;
312- }
313- else if ( textDecorations == TextDecorations . Overline )
314- {
315- targetDecoration = ref this . currentOverline ;
316- }
317- else
318- {
319- return ;
320- }
321-
322272 Pen ? pen = null ;
323273 if ( this . currentTextRun is RichTextRun drawingRun )
324274 {
@@ -339,40 +289,66 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star
339289 }
340290
341291 // Always respect the pen stroke width if explicitly set.
292+ float originalThickness = thickness ;
342293 if ( pen is not null )
343294 {
344- thickness = pen . StrokeWidth ;
295+ // Clamp the thickness to whole pixels.
296+ thickness = MathF . Max ( 1F , ( float ) Math . Round ( pen . StrokeWidth ) ) ;
345297 }
346298 else
347299 {
348- // Clamp the thickness to whole pixels.
349300 // Brush cannot be null if pen is null.
350- thickness = MathF . Max ( 1F , MathF . Round ( thickness ) ) ;
301+ // The thickness of the line has already been clamped in the base class.
351302 pen = new SolidPen ( ( this . currentBrush ?? this . defaultBrush ) ! , thickness ) ;
352303 }
353304
354- // Drawing is always centered around the point so we need to offset by half.
355- Vector2 offset = Vector2 . Zero ;
356- bool rotated = this . currentDecorationIsVertical ;
357- if ( textDecorations == TextDecorations . Overline )
358- {
359- // CSS overline is drawn above the position, so we need to move it up.
360- offset = rotated ? new Vector2 ( thickness * .5F , 0 ) : new Vector2 ( 0 , - ( thickness * .5F ) ) ;
361- }
362- else if ( textDecorations == TextDecorations . Underline )
305+ // Path has already been added to the collection via the base class.
306+ IPath path = this . CurrentPaths . Last ( ) ;
307+ IPath outline = path ;
308+
309+ if ( originalThickness != thickness )
363310 {
364- // CSS underline is drawn below the position, so we need to move it down.
365- offset = rotated ? new Vector2 ( - ( thickness * .5F ) , 0 ) : new Vector2 ( 0 , thickness * .5F ) ;
311+ // Respect edge anchoring per decoration type:
312+ // - Overline: keep the base edge fixed (bottom in horizontal; left in vertical)
313+ // - Underline: keep the top edge fixed (top in horizontal; right in vertical)
314+ // - Strikeout: keep the center fixed (default behavior)
315+ float ratio = thickness / originalThickness ;
316+ if ( ratio != 1f )
317+ {
318+ Vector2 scale = this . currentDecorationIsVertical
319+ ? new Vector2 ( ratio , 1f )
320+ : new Vector2 ( 1f , ratio ) ;
321+
322+ RectangleF b = path . Bounds ;
323+ Vector2 center = new ( b . Left + ( b . Width * 0.5f ) , b . Top + ( b . Height * 0.5f ) ) ;
324+ Vector2 anchor = center ;
325+
326+ if ( textDecorations == TextDecorations . Overline )
327+ {
328+ anchor = this . currentDecorationIsVertical
329+ ? new Vector2 ( b . Left , center . Y ) // vertical: anchor left edge
330+ : new Vector2 ( center . X , b . Bottom ) ; // horizontal: anchor bottom edge
331+ }
332+ else if ( textDecorations == TextDecorations . Underline )
333+ {
334+ anchor = this . currentDecorationIsVertical
335+ ? new Vector2 ( b . Right , center . Y ) // vertical: anchor right edge
336+ : new Vector2 ( center . X , b . Top ) ; // horizontal: anchor top edge
337+ }
338+
339+ // Scale about the chosen anchor so the fixed edge stays in place.
340+ outline = path . Transform ( Matrix3x2 . CreateScale ( scale , anchor ) ) ;
341+ }
366342 }
367343
368- // We clamp the start and end points to the pixel grid to avoid anti-aliasing .
369- this . AppendDecoration (
370- ref targetDecoration ,
371- ClampToPixel ( start + offset , ( int ) thickness , rotated ) ,
372- ClampToPixel ( end + offset , ( int ) thickness , rotated ) ,
373- pen ,
374- thickness ,
375- rotated ) ;
344+ // Render the path here. Decorations are un-cached .
345+ this . DrawingOperations . Add ( new DrawingOperation
346+ {
347+ Brush = pen . StrokeFill ,
348+ RenderLocation = ClampToPixel ( outline . Bounds . Location ) ,
349+ Map = this . Render ( outline ) ,
350+ RenderPass = RenderOrderDecoration
351+ } ) ;
376352 }
377353
378354 protected override void EndGlyph ( )
@@ -506,122 +482,11 @@ private void UpdateCache(GlyphRenderData renderData)
506482 this . glyphCache [ this . currentCacheKey ] . Add ( renderData ) ;
507483 }
508484
509- protected override void EndText ( )
510- {
511- // Ensure we have captured the last overline/underline/strikeout path
512- this . FinalizeDecoration ( ref this . currentOverline ) ;
513- this . FinalizeDecoration ( ref this . currentUnderline ) ;
514- this . FinalizeDecoration ( ref this . currentStrikeout ) ;
515- }
516-
517485 public void Dispose ( ) => this . Dispose ( true ) ;
518486
519487 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
520488 private static Point ClampToPixel ( PointF point ) => Point . Truncate ( point ) ;
521489
522- [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
523- private static PointF ClampToPixel ( PointF point , int thickness , bool rotated )
524- {
525- // Even. Clamp to whole pixels.
526- if ( ( thickness & 1 ) == 0 )
527- {
528- return Point . Truncate ( point ) ;
529- }
530-
531- // Odd. Clamp to half pixels.
532- if ( rotated )
533- {
534- return Point . Truncate ( point ) + new Vector2 ( .5F , 0 ) ;
535- }
536-
537- return Point . Truncate ( point ) + new Vector2 ( 0 , .5F ) ;
538- }
539-
540- private void FinalizeDecoration ( ref TextDecorationDetails ? decoration )
541- {
542- // TODO: Shouldn't we be using the path from the builder here?
543- if ( decoration != null )
544- {
545- // TODO: If the path is curved a line segment does not work well.
546- // What would be great would be if we could take a slice of a path given start and end positions.
547- IPath path = new Path ( new LinearLineSegment ( decoration . Value . Start , decoration . Value . End ) ) ;
548- IPath outline = decoration . Value . Pen . GeneratePath ( path , decoration . Value . Thickness ) ;
549-
550- // Calculate the transform for this path.
551- // We cannot use the path builder transform as this path is rendered independently.
552- FontRectangle rectangle = new ( outline . Bounds . Location , new Vector2 ( outline . Bounds . Width , outline . Bounds . Height ) ) ;
553- Matrix3x2 pathTransform = this . ComputeTransform ( in rectangle ) ;
554- Matrix3x2 defaultTransform = this . drawingOptions . Transform ;
555- outline = outline . Transform ( pathTransform * defaultTransform ) ;
556-
557- if ( outline . Bounds . Width != 0 && outline . Bounds . Height != 0 )
558- {
559- // Render the path here. Decorations are un-cached.
560- this . DrawingOperations . Add ( new DrawingOperation
561- {
562- Brush = decoration . Value . Pen . StrokeFill ,
563- RenderLocation = ClampToPixel ( outline . Bounds . Location ) ,
564- Map = this . Render ( outline ) ,
565- RenderPass = RenderOrderDecoration
566- } ) ;
567- }
568-
569- decoration = null ;
570- }
571- }
572-
573- private void AppendDecoration (
574- ref TextDecorationDetails ? decoration ,
575- Vector2 start ,
576- Vector2 end ,
577- Pen pen ,
578- float thickness ,
579- bool rotated )
580- {
581- if ( decoration != null )
582- {
583- // TODO: This only works well if we are not trying to follow a path.
584- if ( this . path is null )
585- {
586- // Let's try and expand it first.
587- if ( rotated )
588- {
589- if ( thickness == decoration . Value . Thickness
590- && decoration . Value . End . Y + 1 >= start . Y
591- && decoration . Value . End . X == start . X
592- && decoration . Value . Pen . Equals ( pen ) )
593- {
594- // Expand the line
595- start = decoration . Value . Start ;
596-
597- // If this is null finalize does nothing.
598- decoration = null ;
599- }
600- }
601- else if ( thickness == decoration . Value . Thickness
602- && decoration . Value . End . Y == start . Y
603- && decoration . Value . End . X + 1 >= start . X
604- && decoration . Value . Pen . Equals ( pen ) )
605- {
606- // Expand the line
607- start = decoration . Value . Start ;
608-
609- // If this is null finalize does nothing.
610- decoration = null ;
611- }
612- }
613- }
614-
615- this . FinalizeDecoration ( ref decoration ) ;
616- decoration = new TextDecorationDetails
617- {
618- Start = start ,
619- End = end ,
620- Pen = pen ,
621- Thickness = thickness
622- } ;
623- }
624-
625490 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
626491 private void TransformGlyph ( in FontRectangle bounds )
627492 => this . Builder . SetTransform ( this . ComputeTransform ( in bounds ) ) ;
0 commit comments