Browse Source

Redesigned the automated layout issue detection algorithm

Marcin Ziąbek 1 year ago
parent
commit
eec8f6634c

+ 60 - 34
Source/QuestPDF/Drawing/Proxy/Helpers.cs

@@ -8,31 +8,32 @@ namespace QuestPDF.Drawing.Proxy;
 
 internal static class Helpers
 {
-    internal static Size? TryMeasureWithOverflow(this Element element, Size availableSpace)
+    internal static SpacePlan TryMeasureWithOverflow(this Element element, Size availableSpace)
     {
         return TryVerticalOverflow()
                ?? TryHorizontalOverflow() 
-               ?? TryUnconstrainedOverflow();
+               ?? TryUnconstrainedOverflow()
+               ?? SpacePlan.Wrap();
 
-        Size? TryOverflow(Size targetSpace)
+        SpacePlan? TryOverflow(Size targetSpace)
         {
             var contentSize = element.Measure(targetSpace);
             return contentSize.Type == SpacePlanType.Wrap ? null : contentSize;
         }
     
-        Size? TryVerticalOverflow()
+        SpacePlan? TryVerticalOverflow()
         {
             var overflowSpace = new Size(availableSpace.Width, Size.Infinity);
             return TryOverflow(overflowSpace);
         }
     
-        Size? TryHorizontalOverflow()
+        SpacePlan? TryHorizontalOverflow()
         {
             var overflowSpace = new Size(Size.Infinity, availableSpace.Height);
             return TryOverflow(overflowSpace);
         }
     
-        Size? TryUnconstrainedOverflow()
+        SpacePlan? TryUnconstrainedOverflow()
         {
             var overflowSpace = new Size(Size.Infinity, Size.Infinity);
             return TryOverflow(overflowSpace);
@@ -53,47 +54,72 @@ internal static class Helpers
         
         void Traverse(TreeNode<OverflowDebuggingProxy> element)
         {
-            if (element.Value.SpacePlanType is null or SpacePlanType.FullRender)
+            // element was not part of the current layout measurement,
+            // it could not impact the process
+            if (element.Value.SpacePlanType is null)
+                return;
+            
+            // element renders fully,
+            // it could not impact the process
+            if (element.Value.SpacePlanType is SpacePlanType.FullRender)
                 return;
 
+            // when element is partially rendering, it likely has no issues,
+            // however, in certain cases, it may contain a child that is a root cause
             if (element.Value.SpacePlanType is SpacePlanType.PartialRender)
             {
                 foreach (var child in element.Children)
                     Traverse(child);
-                
-                return;
             }
             
-            // strategy: element wrap can be caused by any child that returned wrap
-            if (element.Children.Any(x => x.Value.SpacePlanType == SpacePlanType.Wrap))
-            {
-                if (TryFixChildrenOfType(SpacePlanType.Wrap))
-                    return;
-            }
- 
-            // strategy: there could be more complex inner/hidden layout constraint issue,
-            // if element cannot be successfully drawn on infinite canvas
-            if (element.Value.TryMeasureWithOverflow(element.Value.MeasurementSize) == null)
+            // all of the code below relates to element that is wrapping,
+            // it could be root cause, or contain a child (even deeply nested) that is the root cause
+            
+            // strategy
+            // element does not contain any wrapping elements, no obvious root causes,
+            // if it renders fully with extended space, it is a layout root cause
+            if (element.Children.All(x => x.Value.SpacePlanType is not SpacePlanType.Wrap) && MeasureElementWithExtendedSpace() is SpacePlanType.FullRender)
             {
-                if (TryFixChildrenOfType(SpacePlanType.PartialRender))
-                    return;
+                // so apply the layout overflow proxy
+                element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
+                return;
             }
-  
-            // fixing children does not help, fix the element itself
-            element.Value.RemoveExistingProxies();
-            element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
 
-            bool TryFixChildrenOfType(SpacePlanType spacePlanType)
-            {
-                var suspectedChildren = element.Children.Where(x => x.Value.SpacePlanType == spacePlanType);
-
-                if (!suspectedChildren.Any())
-                    return false;
+            // every time a measurement is made, the layout state is mutated
+            // the previous strategy could modify the layout state
+            // reset layout state by measuring the element with original space
+            element.Value.Measure(element.Value.MeasurementSize); 
+            
+            // strategy:
+            // element contains wrapping children, they are likely the root cause,
+            // traverse them and attempt to fix them
+            foreach (var child in element.Children.Where(x => x.Value.SpacePlanType is SpacePlanType.Wrap).ToArray())
+                Traverse(child);
+                
+            // check if fixing wrapping children helped
+            if (MeasureElementWithExtendedSpace() is not SpacePlanType.Wrap)
+                return;
 
-                foreach (var child in suspectedChildren)
-                    Traverse(child);
+            // reset layout state by measuring the element with original space
+            element.Value.Measure(element.Value.MeasurementSize); // reset state
+            
+            // strategy:
+            // element has layout issues but no obvious/trivial root causes
+            // possibly the problem is in nested children of partial rendering children
+            foreach (var child in element.Children.Where(x => x.Value.SpacePlanType is SpacePlanType.PartialRender).ToArray())
+                Traverse(child);
+                
+            // check if fixing partial children helped
+            if (MeasureElementWithExtendedSpace() is not SpacePlanType.Wrap)
+                return;
+            
+            // none of the attempts above have fixed the layout issue
+            // the element itself is the root cause
+            element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
 
-                return element.Value.TryMeasureWithOverflow(element.Value.MeasurementSize).HasValue;
+            SpacePlanType MeasureElementWithExtendedSpace()
+            {
+                return element.Value.TryMeasureWithOverflow(element.Value.MeasurementSize).Type;
             }
         }
     }

+ 5 - 1
Source/QuestPDF/Elements/LayoutOverflowVisualization.cs

@@ -45,7 +45,11 @@ internal class LayoutOverflowVisualization : ContainerElement, IContentDirection
             skiaCanvasBase.MarkCurrentPageAsHavingLayoutIssues();
         
         // check overflow area
-        var contentSize = Child.TryMeasureWithOverflow(availableSpace) ?? Size.Max;
+        var contentArea = Child.TryMeasureWithOverflow(availableSpace);
+
+        var contentSize = contentArea.Type is SpacePlanType.Wrap
+            ? Size.Max
+            : contentArea;
         
         // draw content
         var translate = ContentDirection == ContentDirection.RightToLeft

+ 1 - 0
Source/QuestPDF/Resources/ReleaseNotes.txt

@@ -24,5 +24,6 @@ Version 2023.12.4
 - Fixed: the TextStyle.LetterSpacing property was not documented correctly, and the FluentAPI code prevented its correct use.
 
 Version 2023.12.5
+- Improvement: redesigned the automated layout issue detection algorithm,
 - Improvement: when drawing content with the Canvas element, the library clips drawn content to the element's boundaries, preventing potential overflow when integrating with other libraries.
 - Improved layout stability.