Browse Source

Improved table paging support

Marcin Ziąbek 4 years ago
parent
commit
82e4dd36f0

+ 75 - 0
QuestPDF.Examples/TableExamples.cs

@@ -0,0 +1,75 @@
+using System;
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples
+{
+    public class TableExamples
+    {
+        public static Random Random { get; } = new Random(0);
+        
+        [Test]
+        public void Example()
+        {
+            RenderingTest
+                .Create()
+                .ProduceImages()
+                .PageSize(PageSizes.A4)
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Padding(25)
+                        .Box()
+                        .Border(2)
+                        .MaxHeight(500)
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.ConstantColumn(100);
+                                columns.RelativeColumn();
+                                columns.ConstantColumn(100);
+                                columns.ConstantColumn(200);
+                            });
+
+                            table.Cell().Row(1).Column(1).ColumnSpan(2).Element(CreateBox("A"));
+                            table.Cell().Row(1).Column(3).Element(CreateBox("B"));
+                            table.Cell().Row(1).Column(4).Element(CreateBox("C"));
+                            
+                            table.Cell().Row(2).Column(1).Element(CreateBox("D"));
+                            table.Cell().Row(2).RowSpan(2).Column(2).Element(CreateBox("E"));
+                            table.Cell().Row(2).RowSpan(3).Column(3).ColumnSpan(2).Element(CreateBox("F"));
+                            
+                            table.Cell().Row(3).RowSpan(2).Column(1).Element(CreateBox("G"));
+                            table.Cell().Row(4).RowSpan(2).Column(2).Element(CreateBox("H"));
+                            table.Cell().Row(5).Column(3).Element(CreateBox("I"));
+                            table.Cell().Row(5).Column(4).Element(CreateBox("J"));
+                            table.Cell().Row(5).RowSpan(2).Column(1).Element(CreateBox("K"));
+                            table.Cell().Row(6).Column(2).ColumnSpan(2).Element(CreateBox("L"));
+                            table.Cell().Row(6).Column(4).Element(CreateBox("M"));
+                        });
+                });
+
+            Action<IContainer> CreateBox(string label)
+            {
+                return container =>
+                {
+                    var height = Random.Next(2, 7) * 25;
+                    
+                    container
+                        .Border(1)
+                        .Background(Placeholders.BackgroundColor())
+                        .AlignCenter()
+                        .AlignMiddle()
+                        .Border(1)
+                        .MinHeight(height)
+                        .Text($"{label}: {height}");
+                };
+            }
+        }
+    }
+}

+ 3 - 3
QuestPDF/Elements/Row.cs

@@ -8,8 +8,8 @@ namespace QuestPDF.Elements
 {
 {
     internal class RowElement : Constrained
     internal class RowElement : Constrained
     {
     {
-        public float ConstantSize { get; set;  }
-        public float RelativeSize { get; set;  }
+        public float ConstantSize { get; }
+        public float RelativeSize { get; }
 
 
         public RowElement(float constantSize, float relativeSize)
         public RowElement(float constantSize, float relativeSize)
         {
         {
@@ -138,7 +138,7 @@ namespace QuestPDF.Elements
             var constantWidth = Children.Sum(x => x.ConstantSize);
             var constantWidth = Children.Sum(x => x.ConstantSize);
             var relativeWidth = Children.Sum(x => x.RelativeSize);
             var relativeWidth = Children.Sum(x => x.RelativeSize);
 
 
-            var widthPerRelativeUnit = (availableWidth - constantWidth) / relativeWidth;
+            var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
             
             
             foreach (var row in Children)
             foreach (var row in Children)
             {
             {

+ 235 - 0
QuestPDF/Elements/Table.cs

@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal class TableColumnDefinition
+    {
+        public float ConstantSize { get;  }
+        public float RelativeSize { get; }
+
+        internal float Width { get; set; }
+
+        public TableColumnDefinition(float constantSize, float relativeSize)
+        {
+            ConstantSize = constantSize;
+            RelativeSize = relativeSize;
+        }
+    }
+    
+    public interface ITableCellContainer : IContainer
+    {
+            
+    }
+    
+    internal class TableCell : Container, ITableCellContainer
+    {
+        public int Row { get; set; } = 1;
+        public int RowSpan { get; set; } = 1;
+
+        public int Column { get; set; } = 1;
+        public int ColumnSpan { get; set; } = 1;
+    }
+
+    internal class TableRenderingPlan
+    {
+        public Size Size { get; set; }
+        public List<TableCellRenderingCommand> CellRenderingCommands { get; set; }
+        public int MaxRowRendered { get; set; }
+    }
+    
+    internal class TableCellRenderingCommand
+    {
+        public TableCell Cell { get; set; }
+        public Size Size { get; set; }
+        public Position Offset { get; set; }
+    }
+    
+    internal class Table : Element, IStateResettable
+    {
+        public ICollection<TableColumnDefinition> Columns { get; } = new List<TableColumnDefinition>();
+        public ICollection<TableCell> Children { get; } = new List<TableCell>();
+        public float Spacing { get; set; }
+        
+        public int CurrentRow { get; set; }
+        
+        internal override void HandleVisitor(Action<Element?> visit)
+        {
+            Children.ToList().ForEach(x => x.HandleVisitor(visit));
+            base.HandleVisitor(visit);
+        }
+        
+        public void ResetState()
+        {
+            CurrentRow = 1;
+        }
+        
+        internal override SpacePlan Measure(Size availableSpace)
+        {
+            UpdateColumnsWidth(availableSpace.Width);
+            
+            var layout = PlanLayout(availableSpace);
+
+            return layout.MaxRowRendered < GetRowsCount() 
+                ? SpacePlan.PartialRender(layout.Size) 
+                : SpacePlan.FullRender(layout.Size);
+        }
+
+        internal override void Draw(Size availableSpace)
+        {
+            UpdateColumnsWidth(availableSpace.Width);
+            var layout = PlanLayout(availableSpace);
+            CurrentRow = layout.MaxRowRendered;
+            
+            foreach (var command in layout.CellRenderingCommands)
+            {
+                Canvas.Translate(command.Offset);
+                command.Cell.Draw(command.Size);
+                Canvas.Translate(command.Offset.Reverse());
+            }
+        }
+        
+        private void UpdateColumnsWidth(float availableWidth)
+        {
+            var constantWidth = Columns.Sum(x => x.ConstantSize);
+            var relativeWidth = Columns.Sum(x => x.RelativeSize);
+
+            var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
+            
+            foreach (var column in Columns)
+            {
+                column.Width = column.ConstantSize + column.RelativeSize * widthPerRelativeUnit;
+            }
+        }
+
+        private TableRenderingPlan PlanLayout(Size availableSpace)
+        {
+            var cellRenderingCommands = new List<TableCellRenderingCommand>();
+            
+            // update row heights
+            var rowsCount = GetRowsCount();
+            var rowBottomOffsets = new float[rowsCount];
+            var childrenToTry = Children.Where(x => x.Row >= CurrentRow).OrderBy(x => x.Row);
+            
+            foreach (var child in childrenToTry)
+            {
+                var rowIndex = child.Row - 1;
+                
+                var topOffset = 0f;
+
+                if (rowIndex > 0)
+                    topOffset = rowBottomOffsets[rowIndex - 1]; // look at previous row
+                
+                var height = GetCellSize(child).Height;
+                var cellBottomOffset = height + topOffset;
+                
+                var targetRowId = child.Row + child.RowSpan - 2; // -1 because indexing starts at 0, -1 because rowSpan starts at 1
+                rowBottomOffsets[targetRowId] = Math.Max(rowBottomOffsets[targetRowId], cellBottomOffset);
+            }
+            
+            Enumerable
+                .Range(1, rowsCount - 1)
+                .ToList()
+                .ForEach(x => rowBottomOffsets[x] = Math.Max(rowBottomOffsets[x], rowBottomOffsets[x-1]));
+            
+            var rowHeights = new float[rowsCount];
+            rowHeights[0] = rowBottomOffsets[0];
+            
+            Enumerable
+                .Range(1, rowsCount - 1)
+                .ToList()
+                .ForEach(x => rowHeights[x] = rowBottomOffsets[x] - rowBottomOffsets[x-1]);
+            
+            // find rows count to render in this pass
+            var rowsToDisplay = rowHeights.Scan((x, y) => x + y).Count(x => x <= availableSpace.Height + Size.Epsilon);
+            rowHeights = rowHeights.Take(rowsToDisplay).ToArray();
+            
+            var totalHeight = rowHeights.Sum();
+            var totalWidth = Columns.Sum(x => x.Width);
+
+            foreach (var cell in Children)
+            {
+                if (!IsCellVisible(cell, rowsToDisplay))
+                    continue;
+
+                var leftOffset = GetCellWidthOffset(cell);
+                var topOffset = rowHeights.Take(cell.Row - 1).Sum();
+
+                var width = GetCellWidth(cell);
+                var height = rowHeights.Skip(cell.Row - 1).Take(cell.RowSpan).Sum();
+
+                cellRenderingCommands.Add(new TableCellRenderingCommand()
+                {
+                    Cell = cell,
+                    Size = new Size(width, height),
+                    Offset = new Position(leftOffset, topOffset)
+                });
+            }
+
+            return new TableRenderingPlan
+            {
+                Size = new Size(totalWidth, totalHeight),
+                CellRenderingCommands = cellRenderingCommands,
+                MaxRowRendered = rowsToDisplay
+            };
+            
+            float GetCellWidthOffset(TableCell cell)
+            {
+                return Columns.Take(cell.Column - 1).Select(x => x.Width).DefaultIfEmpty(0).Sum();
+            }
+
+            float GetCellWidth(TableCell cell)
+            {
+                return Columns.Skip(cell.Column - 1).Take(cell.ColumnSpan).Sum(x => x.Width);   
+            }
+            
+            Size GetCellSize(TableCell cell)
+            {
+                var width = GetCellWidth(cell);
+                var cellSize = new Size(width, Size.Max.Height);
+
+                var measurement = cell.Measure(cellSize);
+
+                if (measurement.Type == SpacePlanType.Wrap)
+                    return new Size(width, Size.Infinity);
+
+                return measurement;
+            }
+
+            bool IsCellVisible(TableCell cell, int maxRow)
+            {
+                return cell.Row <= maxRow;
+            }
+        }
+        
+        int GetRowsCount()
+        {
+            return Children.Max(x => x.Row + x.RowSpan);
+        }
+    }
+    
+    internal static class EnumerableExtensions
+    {
+        public static IEnumerable<T> Scan<T>(this IEnumerable<T> input, Func<T, T, T> accumulate)
+        {
+            using var enumerator = input.GetEnumerator();
+            
+            if (!enumerator.MoveNext())
+                yield break;
+            
+            var state = enumerator.Current;
+            yield return state;
+            
+            while (enumerator.MoveNext())
+            {
+                state = accumulate(state, enumerator.Current);
+                yield return state;
+            }
+        }
+    }
+}

+ 103 - 0
QuestPDF/Fluent/TableExtensions.cs

@@ -0,0 +1,103 @@
+using System;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+    public class TableDefinitionDescriptor
+    {
+        private Table Table { get; }
+
+        internal TableDefinitionDescriptor(Table table)
+        {
+            Table = table;
+        }
+        
+        public void ConstantColumn(float width)
+        {
+            ComplexColumn(constantWidth: width);
+        }
+        
+        public void RelativeColumn(float width = 1)
+        {
+            ComplexColumn(relativeWidth: width);
+        }
+        
+        public void ComplexColumn(float constantWidth = 0, float relativeWidth = 0)
+        {
+            var columnDefinition = new TableColumnDefinition(constantWidth, relativeWidth);
+            Table.Columns.Add(columnDefinition);
+        }
+    }
+
+    public class TableDescriptor
+    {
+        private Table Table { get; }
+
+        internal TableDescriptor(Table table)
+        {
+            Table = table;
+        }
+        
+        public void ColumnsDefinition(Action<TableDefinitionDescriptor> handler)
+        {
+            var descriptor = new TableDefinitionDescriptor(Table);
+            handler(descriptor);
+        }
+        
+        public void Spacing(float value)
+        {
+            Table.Spacing = value;
+        }
+
+        public ITableCellContainer Cell()
+        {
+            var cell = new TableCell();
+            Table.Children.Add(cell);
+            return cell;
+        }
+    }
+    
+    public static class TableExtensions
+    {
+        public static void Table(this IContainer element, Action<TableDescriptor> handler)
+        {
+            var table = new Table();
+            var descriptor = new TableDescriptor(table);
+        
+            handler(descriptor);
+            element.Element(table);
+        }
+    }
+
+    public static class TableCellExtensions
+    {
+        private static ITableCellContainer TableCell(this ITableCellContainer element, Action<TableCell> handler)
+        {
+            if (element is TableCell tableCell)
+                handler(tableCell);
+            
+            return element;
+        }
+        
+        public static ITableCellContainer Column(this ITableCellContainer tableCellContainer, uint value)
+        {
+            return tableCellContainer.TableCell(x => x.Column = (int)value);
+        }
+        
+        public static ITableCellContainer ColumnSpan(this ITableCellContainer tableCellContainer, uint value)
+        {
+            return tableCellContainer.TableCell(x => x.ColumnSpan = (int)value);
+        }
+        
+        public static ITableCellContainer Row(this ITableCellContainer tableCellContainer, uint value)
+        {
+            return tableCellContainer.TableCell(x => x.Row = (int)value);
+        }
+        
+        public static ITableCellContainer RowSpan(this ITableCellContainer tableCellContainer, uint value)
+        {
+            return tableCellContainer.TableCell(x => x.RowSpan = (int)value);
+        }
+    }
+}

+ 1 - 0
QuestPDF/Infrastructure/Size.cs

@@ -3,6 +3,7 @@
     public readonly struct Size
     public readonly struct Size
     {
     {
         public const float Epsilon = 0.001f;
         public const float Epsilon = 0.001f;
+        public const float Infinity = float.PositiveInfinity;
 
 
         public readonly float Width;
         public readonly float Width;
         public readonly float Height;
         public readonly float Height;