Ver Fonte

TextStyleManager redesign

Marcin Ziąbek há 1 ano atrás
pai
commit
b2d9815bda

+ 80 - 8
Source/QuestPDF/Infrastructure/TextStyle.cs

@@ -1,12 +1,17 @@
-using QuestPDF.Helpers;
+using System.Linq;
+using QuestPDF.Helpers;
+using QuestPDF.Skia.Text;
 
 namespace QuestPDF.Infrastructure
 {
     public record TextStyle
     {
+        internal int Id { get; set; }
+        
         internal string? Color { get; set; }
         internal string? BackgroundColor { get; set; }
         internal string? FontFamily { get; set; }
+        internal string? FontFamilyFallback { get; set; }
         internal float? Size { get; set; }
         internal float? LineHeight { get; set; }
         internal float? LetterSpacing { get; set; }
@@ -15,16 +20,20 @@ namespace QuestPDF.Infrastructure
         internal bool? IsItalic { get; set; }
         internal bool? HasStrikethrough { get; set; }
         internal bool? HasUnderline { get; set; }
-        internal bool? WrapAnywhere { get; set; }
         internal TextDirection? Direction { get; set; }
 
-        internal TextStyle? Fallback { get; set; }
-
+        public static TextStyle Default { get; } = new()
+        {
+            Id = 0
+        };
+        
         internal static TextStyle LibraryDefault { get; } = new()
         {
+            Id = 1,
             Color = Colors.Black,
             BackgroundColor = Colors.Transparent,
             FontFamily = Fonts.Lato,
+            FontFamilyFallback = null,
             Size = 12,
             LineHeight = 1.2f,
             LetterSpacing = 0,
@@ -33,11 +42,74 @@ namespace QuestPDF.Infrastructure
             IsItalic = false,
             HasStrikethrough = false,
             HasUnderline = false,
-            WrapAnywhere = false,
-            Direction = TextDirection.Auto,
-            Fallback = null
+            Direction = TextDirection.Auto
         };
 
-        public static TextStyle Default { get; } = new();
+        private SkTextStyle? SkTextStyleCache;
+        
+        internal SkTextStyle GetSkTextStyle()
+        {
+            if (SkTextStyleCache != null)
+                return SkTextStyleCache;
+            
+            SkTextStyleCache = new SkTextStyle(new TextStyleConfiguration
+            {
+                FontSize = CalculateTargetFontSize(),
+                FontWeight = (TextStyleConfiguration.FontWeights?)FontWeight ?? TextStyleConfiguration.FontWeights.Normal,
+                
+                IsItalic = IsItalic ?? false,
+                FontFamily = FontFamily,
+                FontFamilyFallback = FontFamilyFallback,
+                ForegroundColor = Color?.ColorToCode() ?? 0,
+                BackgroundColor = BackgroundColor?.ColorToCode() ?? 0,
+                DecorationColor = Color?.ColorToCode() ?? 0,
+                DecorationType = CreateDecoration(),
+                
+                // TODO: create public API to support these properties
+                DecorationMode = TextStyleConfiguration.TextDecorationMode.Gaps,
+                DecorationStyle = TextStyleConfiguration.TextDecorationStyle.Solid,
+                WordSpacing = 0,
+                
+                LineHeight = LineHeight ?? 1,
+                LetterSpacing = LetterSpacing ?? 0,
+                BaselineOffset = CalculateBaselineOffset(),
+            });
+            
+            return SkTextStyleCache;
+
+            TextStyleConfiguration.TextDecoration CreateDecoration()
+            {
+                var result = TextStyleConfiguration.TextDecoration.NoDecoration;
+                
+                if (HasUnderline == true)
+                    result |= TextStyleConfiguration.TextDecoration.Underline;
+                
+                if (HasStrikethrough == true)
+                    result |= TextStyleConfiguration.TextDecoration.LineThrough;
+                
+                return result;
+            }
+
+            float CalculateTargetFontSize()
+            {
+                var fontSize = Size ?? 0;
+                
+                if (FontPosition is Infrastructure.FontPosition.Subscript or Infrastructure.FontPosition.Superscript)
+                    return fontSize * 0.6f;
+   
+                return fontSize;
+            }
+            
+            float CalculateBaselineOffset()
+            {
+                if (FontPosition == Infrastructure.FontPosition.Subscript)
+                    return Size.Value * 0.25f;
+                
+                if (FontPosition == Infrastructure.FontPosition.Superscript)
+                    return -Size.Value * 0.35f;
+
+                return 0;
+            }
+        }
     }
 }

+ 54 - 194
Source/QuestPDF/Infrastructure/TextStyleManager.cs

@@ -1,5 +1,10 @@
 using System;
 using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using QuestPDF.Skia.Text;
 
 namespace QuestPDF.Infrastructure
 {
@@ -8,6 +13,7 @@ namespace QuestPDF.Infrastructure
         Color,
         BackgroundColor,
         FontFamily,
+        FontFamilyFallback,
         Size,
         LineHeight,
         LetterSpacing,
@@ -16,238 +22,92 @@ namespace QuestPDF.Infrastructure
         IsItalic,
         HasStrikethrough,
         HasUnderline,
-        WrapAnywhere,
         Direction
     }
     
     internal static class TextStyleManager
     {
-        private static readonly ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
-        private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleApplyInheritedCache = new();
-        private static readonly ConcurrentDictionary<TextStyle, TextStyle> TextStyleApplyGlobalCache = new();
-        private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleOverrideCache = new();
+        private static readonly List<TextStyle> TextStyles = new()
+        {
+            TextStyle.Default,
+            TextStyle.LibraryDefault
+        };
+
+        private static readonly List<SkTextStyle> SkTextStyles = new();
+
+        private static readonly ConcurrentDictionary<(int originId, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
+        private static readonly ConcurrentDictionary<(int originId, int parentId), TextStyle> TextStyleApplyInheritedCache = new();
+        private static readonly ConcurrentDictionary<int, TextStyle> TextStyleApplyGlobalCache = new();
+        private static readonly ConcurrentDictionary<(int originId, int parentId), TextStyle> TextStyleOverrideCache = new();
+        
+        private static readonly object MutationLock = new();
 
         public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value)
         {
-            var cacheKey = (origin, property, value);
-            return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.value, overrideValue: true));
+            var cacheKey = (origin.Id, property, value);
+            return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(TextStyles[x.originId], x.property, x.value, overrideValue: true));
         }
 
-        private static TextStyle MutateStyle(this TextStyle origin, TextStyleProperty property, object? value, bool overrideValue)
+        private static TextStyle MutateStyle(this TextStyle origin, TextStyleProperty targetProperty, object? newValue, bool overrideValue)
         {
-            if (overrideValue && value == null)
-                return origin;
-            
-            if (property == TextStyleProperty.Color)
-            {
-                if (!overrideValue && origin.Color != null)
-                    return origin;
-                
-                var castedValue = (string?)value;
-
-                if (origin.Color == castedValue)
-                    return origin;
-
-                return origin with { Color = castedValue };
-            }
-            
-            if (property == TextStyleProperty.BackgroundColor)
-            {
-                if (!overrideValue && origin.BackgroundColor != null)
-                    return origin;
-                
-                var castedValue = (string?)value;
-                
-                if (origin.BackgroundColor == castedValue)
-                    return origin;
-
-                return origin with { BackgroundColor = castedValue };
-            }
-            
-            if (property == TextStyleProperty.FontFamily)
-            {
-                if (!overrideValue && origin.FontFamily != null)
-                    return origin;
-                
-                var castedValue = (string?)value;
-                
-                if (origin.FontFamily == castedValue)
-                    return origin;
-
-                return origin with { FontFamily = castedValue };
-            }
-            
-            if (property == TextStyleProperty.Size)
-            {
-                if (!overrideValue && origin.Size != null)
-                    return origin;
-                
-                var castedValue = (float?)value;
-                
-                if (origin.Size == castedValue)
-                    return origin;
-
-                return origin with { Size = castedValue };
-            }
-            
-            if (property == TextStyleProperty.LineHeight)
-            {
-                if (!overrideValue && origin.LineHeight != null)
-                    return origin;
-                
-                var castedValue = (float?)value;
-                
-                if (origin.LineHeight == castedValue)
-                    return origin;
-
-                return origin with { LineHeight = castedValue };
-            }
-
-            if(property == TextStyleProperty.LetterSpacing)
-            {
-                if (!overrideValue && origin.LetterSpacing != null)
-                    return origin;
-
-                var castedValue = (float?)value;
-
-                if (origin.LetterSpacing == castedValue)
-                    return origin;
-
-                return origin with { LetterSpacing = castedValue };
-            }
-            
-            if (property == TextStyleProperty.FontWeight)
-            {
-                if (!overrideValue && origin.FontWeight != null)
-                    return origin;
-                
-                var castedValue = (FontWeight?)value;
-                
-                if (origin.FontWeight == castedValue)
-                    return origin;
-
-                return origin with { FontWeight = castedValue };
-            }
-            
-            if (property == TextStyleProperty.FontPosition)
-            {
-                if (!overrideValue && origin.FontPosition != null)
-                    return origin;
-                
-                var castedValue = (FontPosition?)value;
-                
-                if (origin.FontPosition == castedValue)
-                    return origin;
-
-                return origin with { FontPosition = castedValue };
-            }
-            
-            if (property == TextStyleProperty.IsItalic)
-            {
-                if (!overrideValue && origin.IsItalic != null)
-                    return origin;
-                
-                var castedValue = (bool?)value;
-                
-                if (origin.IsItalic == castedValue)
-                    return origin;
-
-                return origin with { IsItalic = castedValue };
-            }
-            
-            if (property == TextStyleProperty.HasStrikethrough)
+            lock (MutationLock)
             {
-                if (!overrideValue && origin.HasStrikethrough != null)
+                if (overrideValue && newValue is null)
                     return origin;
                 
-                var castedValue = (bool?)value;
+                var property = typeof(TextStyle).GetProperty(targetProperty.ToString(), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+                Debug.Assert(property != null);
                 
-                if (origin.HasStrikethrough == castedValue)
-                    return origin;
-
-                return origin with { HasStrikethrough = castedValue };
-            }
-            
-            if (property == TextStyleProperty.HasUnderline)
-            {
-                if (!overrideValue && origin.HasUnderline != null)
-                    return origin;
-                
-                var castedValue = (bool?)value;
+                var oldValue = property.GetValue(origin);
                 
-                if (origin.HasUnderline == castedValue)
+                if (!overrideValue && oldValue is not null)
                     return origin;
 
-                return origin with { HasUnderline = castedValue };
-            }
-            
-            if (property == TextStyleProperty.WrapAnywhere)
-            {
-                if (!overrideValue && origin.WrapAnywhere != null)
+                if (oldValue == newValue) 
                     return origin;
-
-                var castedValue = (bool?)value;
                 
-                if (origin.WrapAnywhere == castedValue)
-                    return origin;
-                
-                return origin with { WrapAnywhere = castedValue };
-            }
-
-            if (property == TextStyleProperty.Direction)
-            {
-                if (!overrideValue && origin.Direction != null)
-                    return origin;
+                var newIndex = TextStyles.Count;
+                var newTextStyle = origin with { Id = newIndex };
+                newTextStyle.Id = newIndex;
+                property.SetValue(newTextStyle, newValue);
 
-                var castedValue = (TextDirection?)value;
-                
-                if (origin.Direction == castedValue)
-                    return origin;
-                
-                return origin with { Direction = castedValue };
+                TextStyles.Add(newTextStyle);
+                return newTextStyle;
             }
-
-            throw new ArgumentOutOfRangeException(nameof(property), property, "Expected to mutate the TextStyle object. Provided property type is not supported.");
         }
         
         internal static TextStyle ApplyInheritedStyle(this TextStyle style, TextStyle parent)
         {
-            var cacheKey = (style, parent);
-            return TextStyleApplyInheritedCache.GetOrAdd(cacheKey, key => key.origin.ApplyStyleProperties(key.parent, overrideStyle: false, overrideFontFamily: false));
+            return TextStyleApplyInheritedCache.GetOrAdd((style.Id, parent.Id), key => ApplyStyleProperties(key.originId, key.parentId, overrideStyle: false));
         }
         
         internal static TextStyle ApplyGlobalStyle(this TextStyle style)
         {
-            return TextStyleApplyGlobalCache.GetOrAdd(style, key => key.ApplyStyleProperties(TextStyle.LibraryDefault, overrideStyle: false, overrideFontFamily: false));
+            return TextStyleApplyGlobalCache.GetOrAdd(style.Id, key => ApplyStyleProperties(key, TextStyle.LibraryDefault.Id, overrideStyle: false));
         }
 
         internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
         {
-            var cacheKey = (style, parent);
-            return TextStyleOverrideCache.GetOrAdd(cacheKey, key => ApplyStyleProperties(key.origin, key.parent, overrideStyle: true, overrideFontFamily: true));
+            return TextStyleOverrideCache.GetOrAdd((style.Id, parent.Id), key => ApplyStyleProperties(key.originId, key.parentId, overrideStyle: true));
         }
         
-        private static TextStyle ApplyStyleProperties(this TextStyle style, TextStyle parent, bool overrideStyle, bool overrideFontFamily)
+        private static TextStyle ApplyStyleProperties(int styleId, int parentId, bool overrideStyle)
         {
-            var result = style;
+            var style = TextStyles[styleId];
+            var parent = TextStyles[parentId];
             
-            if (string.IsNullOrWhiteSpace(result.FontFamily) || overrideFontFamily)
-                result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
-                
-            result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.LetterSpacing, parent.LetterSpacing, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle);
-            result = MutateStyle(result, TextStyleProperty.Direction, parent.Direction, overrideStyle);
-
-            return result;
+            return Enum
+                .GetValues(typeof(TextStyleProperty))
+                .Cast<TextStyleProperty>()
+                .Aggregate(style, (mutableStyle, nextProperty) =>
+                {
+                    var getParentProperty = typeof(TextStyle).GetProperty(nextProperty.ToString(), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+                    Debug.Assert(getParentProperty != null);
+                    
+                    var newValue = getParentProperty.GetValue(parent);
+
+                    return mutableStyle.MutateStyle(nextProperty, newValue, overrideStyle);
+                });
         }
     }
 }