Browse Source

Feature: ShowIf (#725)

* ShowWhen: experimental implementation

* Update NativeDependencyCompatibilityChecker.cs

* Add units to row spacing (#711)

This brings it in line with column spacing and allows the user to specify row spacing in units other than points.

* Update README.md

* Optimization: reduced FluentAPI allocations (#717)

* Reduced allocations: Translate Fluent API

* Reduced allocations: Alignment Fluent API

* Reduced allocations: Border Fluent API

* Reduced allocations: Constrained Fluent API

* Reduced allocations: Extend Fluent API

* Reduced allocations: Padding Fluent API

* Reduced allocations: Rotate Fluent API

* Reduced allocations: Scale Fluent API

* Reduced allocations: Table Fluent API

* Generate Image method schema adjustment

* Fixed Constrained Fluent API

* Reduced allocations: Text Fluent API

* Reduced allocations: Text Span Style Fluent API

* Feature: layout tests (#718)

* Prototype implementation

* Layout testing: added support for z-depth

* Layout testing: drawing grid

* Code refactoring

* LayoutTestResultVisualization refactoring

* LayoutTestExecutor refactoring

* LayoutTestValidator refactoring

* Fixed LayoutTestValidator

* Refactored fluent extensions for layout tests

* LayoutTest: minor refactoring

* Layout test: added support for asserting infinite layouts

* Layout test: annotating places of invalid content

* Layout test: added support for drawing occluded mocks

* Layout test: improved lifecycle

* Added dotnet tools

* Update README.md

* Update SECURITY.md

* Fixed DetectSpanPositionExample example

* Update README.md

* fix(#704) + code refactoring of the layout issue page marker

* Fixed: checking SkiaSharp native dependency is not working on simplified Linux distribution (#707)

* Update README.md

* Fix(#457): text rendering fails due to incorrect cache usage (very rare)

* Fix #709: the Row element does not always correctly handle content that has conflicting layout constraints

* Update bug_report.md

* Disabled warning CS1591

* 2023.10.2

* Fixed build

* Shrink API (#722)

* Shrink APIimplementation

* Code refactoring

* Improvements and fixes to the LayoutTestEngine

* Updated Shrink API tests to the new approach

* Fixed build

* Test case (not working yet)

* ShowIf implementation

* Improved layout testing

---------

Co-authored-by: Stephen Clarke <[email protected]>
Marcin Ziąbek 2 years ago
parent
commit
a59486f202

+ 54 - 1
Source/QuestPDF.Examples/ShowOnceExample.cs

@@ -1,4 +1,6 @@
-using NUnit.Framework;
+using System;
+using System.Linq;
+using NUnit.Framework;
 using QuestPDF.Examples.Engine;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
@@ -50,5 +52,56 @@ namespace QuestPDF.Examples
                     });
                 });
         }
+        
+        [Test]
+        public void ShowIf()
+        {
+            RenderingTest
+                .Create()
+                .ProducePdf()
+                .ShowResults()
+                .RenderDocument(container =>
+                {
+                    container.Page(page =>
+                    {
+                        page.Margin(20);
+                        page.Size(PageSizes.A4);
+                        page.PageColor(Colors.White);
+
+                        page.DefaultTextStyle(x => x.FontSize(20));
+                        
+                        page.Header().Text("Show when example").SemiBold();
+                        
+                        page.Content().Column(column =>
+                        {
+                            column.Spacing(10);
+
+                            foreach (var s in Enumerable.Range(0, 10))
+                            {
+                                foreach (var i in Enumerable.Range(0, Random.Shared.Next(10, 50)))
+                                {
+                                    column
+                                        .Item()
+                                        .Height(40)
+                                        .Width(150)
+                                        .Background(Colors.Grey.Lighten3)
+                                        .Text($"{s} - {i}");
+                                }   
+                                
+                                column.Item().PageBreak();
+                                column.Item().ShowIf(x => x.PageNumber % 2 == 0).PageBreak();
+                            }
+                        });
+                        
+                        page.Footer().Text(text =>
+                        {
+                            text.Span("Page ");
+                            text.CurrentPageNumber();
+                            text.Span(" out of ");
+                            text.TotalPages();
+                        });
+                    });
+                });
+        }
     }
 }

+ 75 - 0
Source/QuestPDF.LayoutTests/ShowIfTests.cs

@@ -0,0 +1,75 @@
+namespace QuestPDF.LayoutTests;
+
+public class ShowIfTests
+{
+    [Test]
+    public void Scenario()
+    {
+        LayoutTest
+            .HavingSpaceOfSize(100, 100)
+            .WithContent(content =>
+            {
+                content.Decoration(decoration =>
+                {
+                    decoration.Before().ShowIf(c => c.PageNumber % 2 == 0).Mock("before").Size(80, 20);
+                    decoration.Content().Mock("content").Size(70, 460);
+                    decoration.After().ShowIf(c => c.PageNumber % 3 == 0).Mock("after").Size(90, 30);
+                });
+            })
+            .ExpectedDrawResult(document =>
+            {
+                document
+                    .Page()
+                    .RequiredAreaSize(70, 100)
+                    .Content(page =>
+                    {
+                        page.Mock("content").Position(0, 0).Size(70, 100);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(80, 100)
+                    .Content(page =>
+                    {
+                        page.Mock("before").Position(0, 0).Size(80, 20);
+                        page.Mock("content").Position(0, 20).Size(80, 80);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(90, 100)
+                    .Content(page =>
+                    {
+                        page.Mock("content").Position(0, 0).Size(90, 70);
+                        page.Mock("after").Position(0, 70).Size(90, 30);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(80, 100)
+                    .Content(page =>
+                    {
+                        page.Mock("before").Position(0, 0).Size(80, 20);
+                        page.Mock("content").Position(0, 20).Size(80, 80);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(70, 100)
+                    .Content(page =>
+                    {
+                        page.Mock("content").Position(0, 0).Size(70, 100);
+                    });
+                
+                document
+                    .Page()
+                    .RequiredAreaSize(90, 80)
+                    .Content(page =>
+                    {
+                        page.Mock("before").Position(0, 0).Size(90, 20);
+                        page.Mock("content").Position(0, 20).Size(90, 30);
+                        page.Mock("after").Position(0, 50).Size(90, 30);
+                    });
+            });
+    }
+}

+ 1 - 1
Source/QuestPDF.LayoutTests/ShrinkTests.cs

@@ -20,7 +20,7 @@ public class ShrinkTests
                     .RequiredAreaSize(60, 120)
                     .Content(page =>
                     {
-                        page.Mock().Position(0, 0).Size(60, 120);
+                        page.Mock().Position(0, 0).Size(60, 110);
                     });
                 
                 document

+ 5 - 3
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestExecutor.cs

@@ -42,11 +42,14 @@ internal static class LayoutTestExecutor
         
             while(true)
             {
+                pageContext.IncrementPageNumber();
+              
                 var spacePlan = container.Measure(pageSize);
                 pageSizes.Add(spacePlan);
 
                 if (spacePlan.Type == SpacePlanType.Wrap)
                 {
+                    pageContext.DecrementPageNumber();
                     canvas.EndDocument();
                     return (pageSizes, true);
                 }
@@ -55,8 +58,6 @@ internal static class LayoutTestExecutor
                 {
                     canvas.BeginPage(pageSize);
                     container.Draw(pageSize);
-                
-                    pageContext.IncrementPageNumber();
                 }
                 catch (Exception exception)
                 {
@@ -81,9 +82,10 @@ internal static class LayoutTestExecutor
             return mocks
                 .SelectMany(x => x.DrawingCommands)
                 .GroupBy(x => x.PageNumber)
+                .OrderBy(x => x.Key)
                 .Select(x => new LayoutTestResult.PageLayout
                 {
-                    RequiredArea = pageSizes.ElementAt(x.Key),
+                    RequiredArea = pageSizes.ElementAt(x.Key - 1),
                     Mocks = x
                         .Select(y => new LayoutTestResult.MockLayoutPosition
                         {

+ 1 - 1
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestOutputVisualization.cs

@@ -150,7 +150,7 @@ internal static class LayoutTestResultVisualization
                 DrawMock(mock);
             
             foreach (var mock in pageLayout.Mocks.GetOverlappingItems())
-                DrawOccludedMock(mock.belowMockId);
+                DrawOccludedMock(mock.Below);
             
             DrawGridLines();
         }

+ 1 - 1
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestResult.cs

@@ -31,7 +31,7 @@ internal sealed class LayoutTestResult
 
 internal static class LayoutTestResultHelpers
 {
-    public static IEnumerable<(LayoutTestResult.MockLayoutPosition belowMockId, LayoutTestResult.MockLayoutPosition aboveMockId)> GetOverlappingItems(this ICollection<LayoutTestResult.MockLayoutPosition> items)
+    public static IEnumerable<(LayoutTestResult.MockLayoutPosition Below, LayoutTestResult.MockLayoutPosition Above)> GetOverlappingItems(this ICollection<LayoutTestResult.MockLayoutPosition> items)
     {
         for (var i = 0; i < items.Count; i++)
         {

+ 3 - 3
Source/QuestPDF.LayoutTests/TestEngine/LayoutTestValidator.cs

@@ -64,7 +64,7 @@ internal static class LayoutTestValidator
                     .Count();
 
                 if (matchingActualMock == 0)
-                    throw new Exception($"Cannot find '{expectedMock.MockId}' mock on position {expectedMock.Position} and size {expectedMock.Size}");
+                    throw new Exception($"Cannot find '{expectedMock.MockId}' mock on position {expectedMock.Position.ToString()} and size {expectedMock.Size}");
                 
                 if (matchingActualMock > 1)
                     throw new Exception($"Found multiple '{expectedMock.MockId}' mocks on position {expectedMock.Position} and size {expectedMock.Size}");
@@ -78,10 +78,10 @@ internal static class LayoutTestValidator
             
             foreach (var expectedOverlap in expectedOverlaps)
             {
-                var matchingActualElements = actualOverlaps.Count(actualOverlap => actualOverlap.belowMockId == expectedOverlap.belowMockId && actualOverlap.aboveMockId == expectedOverlap.aboveMockId);
+                var matchingActualElements = actualOverlaps.Count(actualOverlap => actualOverlap.Below.MockId == expectedOverlap.Below.MockId && actualOverlap.Above.MockId == expectedOverlap.Above.MockId);
 
                 if (matchingActualElements != 1)
-                    throw new Exception($"Mock '{expectedOverlap.belowMockId}' should be visible below '{expectedOverlap.aboveMockId}' mock");
+                    throw new Exception($"Mock '{expectedOverlap.Below.MockId}' should be visible below '{expectedOverlap.Above.MockId}' mock");
             }
         }
     }

+ 4 - 1
Source/QuestPDF.LayoutTests/TestEngine/MockChild.cs

@@ -33,7 +33,7 @@ internal class ElementMock : Element
 
         var remainingHeight = TotalHeight - HeightOffset;
 
-        if (remainingHeight == 0)
+        if (remainingHeight < Size.Epsilon)
             return SpacePlan.FullRender(Size.Zero);
         
         if (remainingHeight > availableSpace.Height)
@@ -63,5 +63,8 @@ internal class ElementMock : Element
             Position = new Position(matrix.TransX / matrix.ScaleX, matrix.TransY / matrix.ScaleY),
             Size = availableSpace
         });
+
+        if (HeightOffset > TotalHeight - Size.Epsilon)
+            HeightOffset = 0;
     }
 }

+ 3 - 1
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -194,10 +194,13 @@ namespace QuestPDF.Drawing
 
             while(true)
             {
+                pageContext.IncrementPageNumber();
                 var spacePlan = content.Measure(Size.Max);
 
                 if (spacePlan.Type == SpacePlanType.Wrap)
                 {
+                    pageContext.DecrementPageNumber();
+                    
                     if (Settings.EnableDebugging)
                     {
                         ApplyLayoutDebugging();
@@ -210,7 +213,6 @@ namespace QuestPDF.Drawing
 
                 try
                 {
-                    pageContext.IncrementPageNumber();
                     canvas.BeginPage(spacePlan);
                     content.Draw(spacePlan);
                 }

+ 41 - 0
Source/QuestPDF/Elements/ShowIf.cs

@@ -0,0 +1,41 @@
+using System;
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements;
+
+public class ShowIfContext
+{
+    public int PageNumber { get; internal set; }
+    public int TotalPages { get; internal set; }
+}
+
+internal class ShowIf : ContainerElement
+{
+    public Predicate<ShowIfContext> VisibilityPredicate { get; set; }
+    
+    internal override SpacePlan Measure(Size availableSpace)
+    {
+        if (!CheckVisibility())
+            return SpacePlan.FullRender(Size.Zero);
+
+        return base.Measure(availableSpace);
+    }
+    
+    internal override void Draw(Size availableSpace)
+    {
+        if (CheckVisibility())
+            base.Draw(availableSpace);
+    }
+
+    private bool CheckVisibility()
+    {
+        var context = new ShowIfContext
+        {
+            PageNumber = PageContext.CurrentPage,
+            TotalPages = PageContext.DocumentLength
+        };
+
+        return VisibilityPredicate(context);
+    }
+}

+ 15 - 1
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -275,7 +275,7 @@ namespace QuestPDF.Fluent
         }
         
         /// <summary>
-        /// Conditionally draws or hides its content.
+        /// Conditionally draws or hides its inner content.
         /// <a href="https://www.questpdf.com/api-reference/show-if.html">Learn more</a>
         /// </summary>
         /// <param name="condition">If the value is <see langword="true"/>, its content is visible. Otherwise, it's hidden.</param>
@@ -284,6 +284,20 @@ namespace QuestPDF.Fluent
             return condition ? element : new Container();
         }
         
+        /// <summary>
+        /// Conditionally draws or hides its inner content depending on drawing context.
+        /// Please use carefully as certain predicates may produce unstable layouts resulting with unexpected content or exceptions.
+        /// <a href="https://www.questpdf.com/api-reference/show-if.html">Learn more</a>
+        /// </summary>
+        /// <param name="predicate">If the predicate returns <see langword="true"/>, its content is visible. Otherwise, it's hidden.</param>
+        public static IContainer ShowIf(this IContainer element, Predicate<ShowIfContext> predicate)
+        {
+            return element.Element(new ShowIf
+            {
+                VisibilityPredicate = predicate
+            });
+        }
+        
         /// <summary>
         /// Provides direct access to the low-level SkiaSharp API.
         /// <a href="https://www.questpdf.com/api-reference/canvas.html">Learn more</a>

+ 5 - 0
Source/QuestPDF/Infrastructure/PageContext.cs

@@ -21,6 +21,11 @@ namespace QuestPDF.Infrastructure
             CurrentPage = 0;
         }
         
+        internal void DecrementPageNumber()
+        {
+            CurrentPage--;
+        }
+        
         internal void IncrementPageNumber()
         {
             CurrentPage++;