Browse Source

Ported dynamic component implementation prototype

MarcinZiabek 3 years ago
parent
commit
8a1be90c4f

+ 169 - 0
QuestPDF.Examples/DynamicExamples.cs

@@ -0,0 +1,169 @@
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using QuestPDF.Elements;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples
+{
+    public class OrderItem
+    {
+        public string ItemName { get; set; } = Placeholders.Label();
+        public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10;
+        public int Count { get; set; } = Placeholders.Random.Next(1, 11);
+    }
+    
+    public class OrdersTable : IDynamicComponent
+    {
+        private ICollection<OrderItem> Items { get; }
+        private ICollection<OrderItem> ItemsLeft { get; set; }
+        
+        public OrdersTable(ICollection<OrderItem> items)
+        {
+            Items = items;
+        }
+        
+        public void Compose(DynamicContext context, IDynamicContainer container)
+        {
+            if (context.Operation == DynamicLayoutOperation.Reset)
+            {
+                ItemsLeft = new List<OrderItem>(Items);
+                return;
+            }
+
+            var header = ComposeHeader(context);
+            var sampleFooter = ComposeFooter(context, Enumerable.Empty<OrderItem>());
+            var decorationHeight = header.Size.Height + sampleFooter.Size.Height;
+            
+            var rows = GetItemsForPage(context, decorationHeight).ToList();
+            var footer = ComposeFooter(context, rows.Select(x => x.Item));
+
+            if (ItemsLeft.Count > rows.Count)
+                container.HasMoreContent();
+            
+            if (context.Operation == DynamicLayoutOperation.Draw)
+                ItemsLeft = ItemsLeft.Skip(rows.Count).ToList();
+
+            container.MinimalBox().Decoration(decoration =>
+            {
+                decoration.Header().Element(header);
+                
+                decoration.Content().Box().Stack(stack =>
+                {
+                    foreach (var row in rows)
+                        stack.Item().Element(row.Element);
+                });
+
+                decoration.Footer().Element(footer);
+            });
+        }
+
+        private IDynamicElement ComposeHeader(DynamicContext context)
+        {
+            return context.CreateElement(element =>
+            {
+                element
+                    .BorderBottom(1)
+                    .BorderColor(Colors.Grey.Darken2)
+                    .Padding(5)
+                    .Row(row =>
+                    {
+                        var textStyle = TextStyle.Default.SemiBold();
+
+                        row.ConstantItem(30).Text("#", textStyle);
+                        row.RelativeItem().Text("Item name", textStyle);
+                        row.ConstantItem(50).AlignRight().Text("Count", textStyle);
+                        row.ConstantItem(50).AlignRight().Text("Price", textStyle);
+                        row.ConstantItem(50).AlignRight().Text("Total", textStyle);
+                    });
+            });
+        }
+        
+        private IDynamicElement ComposeFooter(DynamicContext context, IEnumerable<OrderItem> items)
+        {
+            var total = items.Sum(x => x.Count * x.Price);
+
+            return context.CreateElement(element =>
+            {
+                element
+                    .Padding(5)
+                    .AlignRight()
+                    .Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold());
+            });
+        }
+        
+        private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight)
+        {
+            var totalHeight = decorationHeight;
+            var counter = Items.Count - ItemsLeft.Count + 1;
+            
+            foreach (var orderItem in ItemsLeft)
+            {
+                var element = context.CreateElement(content =>
+                {
+                    content
+                        .BorderBottom(1)
+                        .BorderColor(Colors.Grey.Lighten2)
+                        .Padding(5)
+                        .Row(row =>
+                        {
+                            row.ConstantItem(30).Text(counter++);
+                            row.RelativeItem().Text(orderItem.ItemName);
+                            row.ConstantItem(50).AlignRight().Text(orderItem.Count);
+                            row.ConstantItem(50).AlignRight().Text($"{orderItem.Price}$");
+                            row.ConstantItem(50).AlignRight().Text($"{orderItem.Count*orderItem.Price}$");
+                        });
+                });
+
+                var elementHeight = element.Size.Height;
+                    
+                if (totalHeight + elementHeight > context.AvailableSize.Height)
+                    break;
+                    
+                totalHeight += elementHeight;
+                yield return (orderItem, element);
+            }
+        }
+    }
+    
+    public static class DynamicExamples
+    {
+        [Test]
+        public static void Dynamic()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A5)
+                .ShowResults()
+                .Render(container =>
+                {
+                    var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList();
+                    
+                    container
+                        .Background(Colors.White)
+                        .Padding(25)
+                        .Decoration(decoration =>
+                        {
+                            decoration
+                                .Header()
+                                .PaddingBottom(5)
+                                .Text(text =>
+                                {
+                                    text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2).FontSize(16));
+                                    text.Span("Page ");
+                                    text.CurrentPageNumber();
+                                    text.Span(" of ");
+                                    text.TotalPages();
+                                });
+                            
+                            decoration
+                                .Content()
+                                .Dynamic(new OrdersTable(items));
+                        });
+                });
+        }
+    }
+}

+ 4 - 1
QuestPDF/Drawing/DocumentGenerator.cs

@@ -164,7 +164,7 @@ namespace QuestPDF.Drawing
             return debuggingState;
         }
 
-        private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
+        internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
         {
             if (content == null)
                 return;
@@ -186,6 +186,9 @@ namespace QuestPDF.Drawing
                 return;
             }
 
+            if (content is DynamicHost dynamicHost)
+                dynamicHost.TextStyle = documentDefaultTextStyle;
+            
             var targetTextStyle = documentDefaultTextStyle;
             
             if (content is DefaultTextStyle defaultTextStyleElement)

+ 125 - 0
QuestPDF/Elements/Dynamic.cs

@@ -0,0 +1,125 @@
+using System;
+using QuestPDF.Drawing;
+using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal class DynamicHost : Element, IStateResettable
+    {
+        private IDynamicComponent Child { get; }
+        internal TextStyle TextStyle { get; set; }
+
+        public DynamicHost(IDynamicComponent child)
+        {
+            Child = child;
+        }
+
+        public void ResetState()
+        {
+            GetContent(Size.Zero, DynamicLayoutOperation.Reset);
+        }
+        
+        internal override SpacePlan Measure(Size availableSpace)
+        {
+            var content = GetContent(availableSpace, DynamicLayoutOperation.Measure);
+            var measurement = content.Measure(availableSpace);
+
+            if (measurement.Type != SpacePlanType.FullRender)
+                throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page.");
+            
+            return content.HasMoreContent 
+                ? SpacePlan.PartialRender(measurement) 
+                : SpacePlan.FullRender(measurement);
+        }
+
+        internal override void Draw(Size availableSpace)
+        {
+            GetContent(availableSpace, DynamicLayoutOperation.Draw).Draw(availableSpace);
+        }
+
+        DynamicContainer GetContent(Size availableSize, DynamicLayoutOperation operation)
+        {
+            var context = new DynamicContext
+            {
+                PageNumber = PageContext.CurrentPage,
+                PageContext = PageContext,
+                Canvas = Canvas,
+                TextStyle = TextStyle,
+                
+                AvailableSize = availableSize,
+                Operation = operation
+            };
+            
+            var container = new DynamicContainer();
+            Child.Compose(context, container);
+            
+            container.VisitChildren(x => x?.Initialize(PageContext, Canvas));
+            container.VisitChildren(x => (x as IStateResettable)?.ResetState());
+            
+            return container;
+        }
+    }
+
+    public enum DynamicLayoutOperation
+    {
+        Reset,
+        Measure,
+        Draw
+    }
+    
+    public class DynamicContext
+    {
+        internal IPageContext PageContext { get; set; }
+        internal ICanvas Canvas { get; set; }
+        internal TextStyle TextStyle { get; set; }
+        
+        public int PageNumber { get; internal set; }
+        public Size AvailableSize { get; internal set; }
+        public DynamicLayoutOperation Operation { get; internal set; }
+        
+        public IDynamicElement CreateElement(Action<IContainer> content)
+        {
+            var container = new DynamicElement();
+            content(container);
+            
+            container.ApplyDefaultTextStyle(TextStyle);
+            container.VisitChildren(x => x?.Initialize(PageContext, Canvas));
+            container.VisitChildren(x => (x as IStateResettable)?.ResetState());
+
+            container.Size = container.Measure(AvailableSize);
+            
+            return container;
+        }
+    }
+
+    public interface IDynamicContainer : IContainer
+    {
+        
+    }
+
+    internal class DynamicContainer : Container, IDynamicContainer
+    {
+        internal bool HasMoreContent { get; set; }
+    }
+
+    public static class DynamicContainerExtensions
+    {
+        public static IDynamicContainer HasMoreContent(this IDynamicContainer container)
+        {
+            (container as DynamicContainer).HasMoreContent = true;
+            return container;
+        }
+    }
+    
+    public interface IDynamicElement : IElement
+    {
+        Size Size { get; }
+    }
+
+    internal class DynamicElement : ContainerElement, IDynamicElement
+    {
+        public Size Size { get; internal set; }
+    }
+}

+ 18 - 0
QuestPDF/Fluent/DynamicComponentExtensions.cs

@@ -0,0 +1,18 @@
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+    public static class DynamicComponentExtensions
+    {
+        public static void Dynamic<TDynamic>(this IContainer element) where TDynamic : IDynamicComponent, new()
+        {
+            element.Dynamic(new TDynamic());
+        }
+
+        public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement)
+        {
+            element.Element(new DynamicHost(dynamicElement));
+        }
+    }
+}

+ 9 - 0
QuestPDF/Infrastructure/IDynamicComponent.cs

@@ -0,0 +1,9 @@
+using QuestPDF.Elements;
+
+namespace QuestPDF.Infrastructure
+{
+    public interface IDynamicComponent
+    {
+        void Compose(DynamicContext context, IDynamicContainer container);
+    }
+}