Browse Source

Feat: enhanced layout issue debugging (#918)

* Feat: enhanced layout issue debugging experience (initial implementation)

* Code refactoring

* Code refactoring

* Fixed in-code documentation
Marcin Ziąbek 1 year ago
parent
commit
35a5b926f6

+ 16 - 0
Source/QuestPDF/CompatibilitySuppressions.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
+<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:QuestPDF.Fluent.ElementExtensions.Element(QuestPDF.Infrastructure.IContainer,System.Action{QuestPDF.Infrastructure.IContainer},System.String,System.String,System.Int32)</Target>
+    <Left>lib/netstandard2.0/QuestPDF.dll</Left>
+    <Right>lib/net6.0/QuestPDF.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:QuestPDF.Fluent.ElementExtensions.Element(QuestPDF.Infrastructure.IContainer,System.Func{QuestPDF.Infrastructure.IContainer,QuestPDF.Infrastructure.IContainer},System.String,System.String,System.Int32)</Target>
+    <Left>lib/netstandard2.0/QuestPDF.dll</Left>
+    <Right>lib/net6.0/QuestPDF.dll</Right>
+  </Suppression>
+</Suppressions>

+ 2 - 0
Source/QuestPDF/Drawing/DocumentContainer.cs

@@ -21,6 +21,8 @@ namespace QuestPDF.Drawing
             {
                 if (Pages.Count == 0)
                     return;
+
+                container = container.DebugPointer(DebugPointerType.LayoutStructure, "Document");
                 
                 if (Pages.Count == 1)
                 {

+ 28 - 2
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -232,7 +232,7 @@ namespace QuestPDF.Drawing
                 content.ApplyLayoutOverflowDetection();
                 content.Measure(Size.Max);
 
-                var overflowState = content.ExtractElementsOfType<OverflowDebuggingProxy>().FirstOrDefault();
+                var overflowState = content.ExtractElementsOfType<OverflowDebuggingProxy>().Single();
                 overflowState.ApplyLayoutOverflowVisualization();
                 
                 content.ApplyContentDirection();
@@ -243,6 +243,29 @@ namespace QuestPDF.Drawing
             
             void ThrowLayoutException()
             {
+                content.RemoveExistingProxies();
+                content.ApplyLayoutOverflowDetection();
+                content.Measure(Size.Max);
+                
+                var overflowState = content.ExtractElementsOfType<OverflowDebuggingProxy>().Single();
+                overflowState.CaptureOriginalMeasurementValues();
+                overflowState.ApplyLayoutOverflowVisualization();
+
+                var rootCause = overflowState.FindLayoutOverflowVisualizationNodes().First();
+                
+                var stack = rootCause
+                    .ExtractAncestors()
+                    .Select(x => x.Value)
+                    .Reverse()
+                    .FormatAncestors();
+
+                var inside = rootCause
+                    .ExtractAncestors()
+                    .First(x => x.Value.Child is SourceCodePointer)
+                    .Children
+                    .First()
+                    .FormatLayoutSubtree();
+                
                 var newLine = "\n";
                 var newParagraph = newLine + newLine;
                     
@@ -251,7 +274,10 @@ namespace QuestPDF.Drawing
                     $"For example, some elements may require more space than is available. {newParagraph}" +
                     $"To quickly determine the place where the problem is likely occurring, please generate the document with the attached debugger. " +
                     $"The library will generate a PDF document with visually annotated places where layout constraints are invalid. {newParagraph}" +
-                    $"Alternatively, if you don’t want to or cannot attach the debugger, you can set the {nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.EnableDebugging)} flag to true.";
+                    $"Alternatively, if you don’t want to or cannot attach the debugger, you can set the {nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.EnableDebugging)} flag to true. {newParagraph}" +
+                    $"The layout issue is likely present in the following part of the document: {newParagraph}{stack}{newParagraph}" +
+                    $"Please analyse the document measurement to learn more: {newParagraph}{inside}" +
+                    $"{LayoutDebugging.LayoutVisualizationLegend}";
                 
                 throw new DocumentLayoutException(message);
             }

+ 0 - 169
Source/QuestPDF/Drawing/Proxy/Helpers.cs

@@ -1,169 +0,0 @@
-using System.Linq;
-using System.Text;
-using QuestPDF.Elements;
-using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Drawing.Proxy;
-
-internal static class Helpers
-{
-    internal static SpacePlan TryMeasureWithOverflow(this Element element, Size availableSpace)
-    {
-        return TryVerticalOverflow()
-               ?? TryHorizontalOverflow() 
-               ?? TryUnconstrainedOverflow()
-               ?? SpacePlan.Wrap();
-
-        SpacePlan? TryOverflow(Size targetSpace)
-        {
-            var contentSize = element.Measure(targetSpace);
-            return contentSize.Type == SpacePlanType.Wrap ? null : contentSize;
-        }
-    
-        SpacePlan? TryVerticalOverflow()
-        {
-            var overflowSpace = new Size(availableSpace.Width, Size.Infinity);
-            return TryOverflow(overflowSpace);
-        }
-    
-        SpacePlan? TryHorizontalOverflow()
-        {
-            var overflowSpace = new Size(Size.Infinity, availableSpace.Height);
-            return TryOverflow(overflowSpace);
-        }
-    
-        SpacePlan? TryUnconstrainedOverflow()
-        {
-            var overflowSpace = new Size(Size.Infinity, Size.Infinity);
-            return TryOverflow(overflowSpace);
-        }
-    }
-    
-    public static void ApplyLayoutOverflowDetection(this Element container)
-    {
-        container.VisitChildren(x =>
-        {
-            x.CreateProxy(y => y is ElementProxy ? y : new OverflowDebuggingProxy(y));
-        });
-    }
-    
-    public static void ApplyLayoutOverflowVisualization(this TreeNode<OverflowDebuggingProxy> hierarchyRoot)
-    {
-        Traverse(hierarchyRoot);
-        
-        void Traverse(TreeNode<OverflowDebuggingProxy> element)
-        {
-            // before assessing the element,
-            // reset layout state by measuring the element with original space
-            // in case when parent has altered the layout state with different overflow test
-            element.Value.Measure(element.Value.MeasurementSize);
-            
-            // 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;
-            }
-            
-            // 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)
-            {
-                // so apply the layout overflow proxy
-                element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
-                return;
-            }
-
-            // 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))
-                Traverse(child);
-                
-            // check if fixing wrapping children helped
-            if (MeasureElementWithExtendedSpace() is not SpacePlanType.Wrap)
-                return;
-
-            // 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))
-                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 });
-
-            SpacePlanType MeasureElementWithExtendedSpace()
-            {
-                return element.Value.TryMeasureWithOverflow(element.Value.MeasurementSize).Type;
-            }
-        }
-    }
-
-    public static void RemoveExistingProxies(this Element content)
-    {
-        content.VisitChildren(x =>
-        {
-            x.CreateProxy(y => y is ElementProxy proxy ? proxy.Child : y);
-        });
-    }
-
-    public static string HierarchyToString(this Element root)
-    {
-        var indentationCache = Enumerable.Range(0, 128).Select(x => new string(' ', x)).ToArray();
-        
-        var indentationLevel = 0;
-        var result = new StringBuilder();
-        
-        Traverse(root);
-
-        return result.ToString();
-        
-        void Traverse(Element parent)
-        {
-            var elementName = (parent as DebugPointer)?.Target ?? parent.GetType().Name;
-            
-            result.AppendLine();
-            result.Append(indentationCache[indentationLevel]);
-            result.Append(elementName);
-
-            indentationLevel++;
-            
-            foreach (var child in parent.GetChildren())
-                Traverse(child);
-
-            indentationLevel--;
-        }
-    }
-}

+ 320 - 0
Source/QuestPDF/Drawing/Proxy/LayoutDebugging.cs

@@ -0,0 +1,320 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using QuestPDF.Elements;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing.Proxy;
+
+internal static class LayoutDebugging
+{
+    internal static SpacePlan TryMeasureWithOverflow(this Element element, Size availableSpace)
+    {
+        return TryVerticalOverflow()
+               ?? TryHorizontalOverflow() 
+               ?? TryUnconstrainedOverflow()
+               ?? SpacePlan.Wrap();
+
+        SpacePlan? TryOverflow(Size targetSpace)
+        {
+            var contentSize = element.Measure(targetSpace);
+            return contentSize.Type == SpacePlanType.Wrap ? null : contentSize;
+        }
+    
+        SpacePlan? TryVerticalOverflow()
+        {
+            var overflowSpace = new Size(availableSpace.Width, Size.Infinity);
+            return TryOverflow(overflowSpace);
+        }
+    
+        SpacePlan? TryHorizontalOverflow()
+        {
+            var overflowSpace = new Size(Size.Infinity, availableSpace.Height);
+            return TryOverflow(overflowSpace);
+        }
+    
+        SpacePlan? TryUnconstrainedOverflow()
+        {
+            var overflowSpace = new Size(Size.Infinity, Size.Infinity);
+            return TryOverflow(overflowSpace);
+        }
+    }
+    
+    public static void ApplyLayoutOverflowDetection(this Element container)
+    {
+        container.VisitChildren(x =>
+        {
+            x.CreateProxy(y => y is ElementProxy ? y : new OverflowDebuggingProxy(y));
+        });
+    }
+    
+    public static void ApplyLayoutOverflowVisualization(this TreeNode<OverflowDebuggingProxy> hierarchyRoot)
+    {
+        Traverse(hierarchyRoot);
+        
+        void Traverse(TreeNode<OverflowDebuggingProxy> element)
+        {
+            if (element.Value.MeasurementSize is null)
+                return;
+            
+            // before assessing the element,
+            // reset layout state by measuring the element with original space
+            // in case when parent has altered the layout state with different overflow test
+            element.Value.Measure(element.Value.MeasurementSize.Value);
+            
+            // element was not part of the current layout measurement,
+            // it could not impact the process
+            if (element.Value.SpacePlan is null)
+                return;
+            
+            // element renders fully,
+            // it could not impact the process
+            if (element.Value.SpacePlan?.Type 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.SpacePlan?.Type is SpacePlanType.PartialRender)
+            {
+                foreach (var child in element.Children)
+                    Traverse(child);
+                
+                return;
+            }
+            
+            // 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.SpacePlan?.Type is not SpacePlanType.Wrap) && MeasureElementWithExtendedSpace() is SpacePlanType.FullRender)
+            {
+                // so apply the layout overflow proxy
+                element.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
+                return;
+            }
+
+            // 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.Value); 
+            
+            // 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.SpacePlan?.Type is SpacePlanType.Wrap))
+                Traverse(child);
+                
+            // check if fixing wrapping children helped
+            if (MeasureElementWithExtendedSpace() is not SpacePlanType.Wrap)
+                return;
+
+            // reset layout state by measuring the element with original space
+            element.Value.Measure(element.Value.MeasurementSize.Value); // 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.SpacePlan?.Type is SpacePlanType.PartialRender))
+                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 });
+
+            SpacePlanType MeasureElementWithExtendedSpace()
+            {
+                return element.Value.TryMeasureWithOverflow(element.Value.MeasurementSize.Value).Type;
+            }
+        }
+    }
+
+    public static void RemoveExistingProxies(this Element content)
+    {
+        content.VisitChildren(x =>
+        {
+            x.CreateProxy(y => y is ElementProxy proxy ? proxy.Child : y);
+        });
+    }
+
+    public static void CaptureOriginalMeasurementValues(this TreeNode<OverflowDebuggingProxy> parent)
+    {
+        parent.Value.CaptureOriginalValues();
+            
+        foreach (var child in parent.Children)
+            CaptureOriginalMeasurementValues(child);
+    }
+    
+    public static IEnumerable<TreeNode<OverflowDebuggingProxy>> FindLayoutOverflowVisualizationNodes(this TreeNode<OverflowDebuggingProxy> rootNode)
+    {
+        var result = new List<TreeNode<OverflowDebuggingProxy>>();
+        Traverse(rootNode);
+        return result;
+        
+        void Traverse(TreeNode<OverflowDebuggingProxy> node)
+        {
+            if (node.Value.Child is LayoutOverflowVisualization)
+                result.Add(node);
+
+            foreach (var child in node.Children)
+                Traverse(child);
+        }
+    }
+    
+    public static string FormatAncestors(this IEnumerable<OverflowDebuggingProxy> ancestors)
+    {
+        var result = new StringBuilder();
+        
+        foreach (var ancestor in ancestors)
+            Format(ancestor);
+        
+        return result.ToString();
+
+        void Format(OverflowDebuggingProxy node)
+        {
+            if (node.Child is DebugPointer debugPointer)
+            {
+                result.AppendLine($"-> {debugPointer.Label}");
+            }
+            else if (node.Child is SourceCodePointer sourceCodePointer)
+            {
+                result.AppendLine($"-> In method:   {sourceCodePointer.HandlerName}");
+                result.AppendLine($"   Called from: {sourceCodePointer.ParentName}");
+                result.AppendLine($"   Source path: {sourceCodePointer.SourceFilePath}");
+                result.AppendLine($"   Line number: {sourceCodePointer.SourceLineNumber}");
+            }
+            else
+            {
+                return;
+            }
+            
+            result.AppendLine();
+        }
+    }
+    
+    public static string FormatLayoutSubtree(this TreeNode<OverflowDebuggingProxy> root)
+    {
+        var indentationCache = Enumerable.Range(0, 128).Select(x => x * 3).Select(x => new string(' ', x)).ToArray();
+        
+        var indentationLevel = 0;
+        var result = new StringBuilder();
+        
+        Traverse(root);
+        
+        return result.ToString();
+
+        void Traverse(TreeNode<OverflowDebuggingProxy> parent)
+        {
+            var proxy = parent.Value;
+
+            if (proxy.Child is Container)
+            {
+                Traverse(parent.Children.First());
+                return;
+            }
+            
+            if (proxy.OriginalMeasurementSize is null || proxy.OriginalSpacePlan is null)
+                return;
+            
+            var indent = indentationCache[indentationLevel];
+            
+            foreach (var content in Format(proxy))
+                result.AppendLine($"{indent}{content}");
+            
+            result.AppendLine();
+            result.AppendLine();
+            
+            indentationLevel++;
+            
+            foreach (var child in parent.Children)
+                Traverse(child);
+
+            indentationLevel--;
+        }
+
+        static IEnumerable<string> Format(OverflowDebuggingProxy proxy)
+        {
+            var child = proxy.Child;
+            
+            if (child is LayoutOverflowVisualization layoutOverflowVisualization)
+                child = layoutOverflowVisualization.Child;
+
+            var title = GetTitle();
+            yield return title;
+            
+            yield return new string('=', title.Length + 1);
+            
+            yield return $"Available Space: {proxy.OriginalMeasurementSize}";
+            yield return $"Space Plan: {proxy.OriginalSpacePlan}";
+            
+            yield return new string('-', title.Length + 1);
+            
+            foreach (var configuration in GetElementConfiguration(child))
+                yield return $"{configuration}";
+            
+            string GetTitle()
+            {
+                var elementName = child.GetType().Name;
+                
+                if (proxy.Child is LayoutOverflowVisualization)
+                    return $"🚨 {elementName} 🚨";
+                
+                var indicator = proxy.OriginalSpacePlan.Value.Type switch
+                {
+                    SpacePlanType.Wrap => "🔴",
+                    SpacePlanType.PartialRender => "🟡",
+                    SpacePlanType.FullRender => "🟢",
+                    _ => "-"
+                };
+                
+                return $"{indicator} {elementName}";
+            }
+        }
+        
+        static IEnumerable<string> GetElementConfiguration(IElement element)
+        {
+            if (element is DebugPointer)
+                return Enumerable.Empty<string>();
+                
+            return element
+                .GetType()
+                .GetProperties()
+                .Select(x => new
+                {
+                    Property = x.Name.PrettifyName(),
+                    Value = x.GetValue(element)
+                })
+                .Where(x => !(x.Value is IElement))
+                .Where(x => x.Value is string || !(x.Value is IEnumerable))
+                .Where(x => !(x.Value is TextStyle))
+                .Select(x => $"{x.Property}: {FormatValue(x.Value)}");
+
+            string FormatValue(object value)
+            {
+                const int maxLength = 100;
+                    
+                var text = value?.ToString() ?? "-";
+
+                if (text.Length < maxLength)
+                    return text;
+
+                return text.Substring(0, maxLength) + "...";
+            }
+        }
+    }
+    
+    public const string LayoutVisualizationLegend =
+        "Legend: \n" +
+        "🚨 - Element that is likely the root cause of the layout issue based on library heuristics and prediction. \n" +
+        "🔴 - Element that cannot be drawn due to the provided layout constraints. This element likely causes the layout issue, or one of its descendant children is responsible for the problem. \n" +
+        "🟡 - Element that can be partially drawn on the page and will also be rendered on the consecutive page. In more complex layouts, this element may also cause issues or contain a child that is the actual root cause.\n" +
+        "🟢 - Element that is successfully and completely drawn on the page.\n";
+}

+ 12 - 3
Source/QuestPDF/Drawing/Proxy/OverflowDebuggingProxy.cs

@@ -4,8 +4,11 @@ namespace QuestPDF.Drawing.Proxy;
 
 internal class OverflowDebuggingProxy : ElementProxy
 {
-    public Size MeasurementSize { get; private set; }
-    public SpacePlanType? SpacePlanType { get; private set; }
+    public Size? OriginalMeasurementSize { get; private set; }
+    public Size? MeasurementSize { get; private set; }
+    
+    public SpacePlan? OriginalSpacePlan { get; private set; }
+    public SpacePlan? SpacePlan { get; private set; }
 
     public OverflowDebuggingProxy(Element child)
     {
@@ -17,8 +20,14 @@ internal class OverflowDebuggingProxy : ElementProxy
         var spacePlan = Child.Measure(availableSpace);
 
         MeasurementSize = availableSpace;
-        SpacePlanType = spacePlan.Type;
+        SpacePlan = spacePlan;
         
         return spacePlan;
     }
+
+    internal void CaptureOriginalValues()
+    {
+        OriginalMeasurementSize = MeasurementSize;
+        OriginalSpacePlan = SpacePlan;
+    }
 }

+ 18 - 1
Source/QuestPDF/Drawing/Proxy/TreeTraversal.cs

@@ -7,6 +7,7 @@ namespace QuestPDF.Drawing.Proxy;
 internal class TreeNode<T>
 {
     public T Value { get; }
+    public TreeNode<T>? Parent { get; set; }
     public ICollection<TreeNode<T>> Children { get; } = new List<TreeNode<T>>();
     
     public TreeNode(T Value)
@@ -22,9 +23,12 @@ internal static class TreeTraversal
         if (element is T proxy)
         {
             var result = new TreeNode<T>(proxy);
-                
+
             foreach (var treeNode in proxy.GetChildren().SelectMany(ExtractElementsOfType<T>))
+            {
                 result.Children.Add(treeNode);
+                treeNode.Parent = result;
+            }
                 
             yield return result;
         }
@@ -43,4 +47,17 @@ internal static class TreeTraversal
             foreach (var innerChild in Flatten(child))
                 yield return innerChild;
     }
+    
+    public static IEnumerable<TreeNode<T>> ExtractAncestors<T>(this TreeNode<T> node)
+    {
+        while (true)
+        {
+            node = node.Parent;
+            
+            if (node is null)
+                yield break;
+
+            yield return node;
+        }
+    }
 }

+ 9 - 2
Source/QuestPDF/Elements/DebugPointer.cs

@@ -1,8 +1,15 @@
 namespace QuestPDF.Elements
 {
+    internal enum DebugPointerType
+    {
+        LayoutStructure,
+        Component,
+        UserDefined
+    }
+    
     internal sealed class DebugPointer : Container
     {
-        public string Target { get; set; }
-        public bool Highlight { get; set; }
+        public DebugPointerType Type { get; set; }
+        public string Label { get; set; }
     }
 }

+ 7 - 7
Source/QuestPDF/Elements/Page.cs

@@ -37,7 +37,7 @@ namespace QuestPDF.Elements
                 {
                     layers
                         .Layer()
-                        .DebugPointer("Page background layer")
+                        .DebugPointer(DebugPointerType.LayoutStructure, "Page background layer")
                         .Repeat()
                         .Element(Background);
                     
@@ -58,25 +58,25 @@ namespace QuestPDF.Elements
                         {
                             decoration
                                 .Before()
-                                .DebugPointer("Page header")
+                                .DebugPointer(DebugPointerType.LayoutStructure, "Page header")
                                 .Element(Header);
 
                             decoration
                                 .Content()
-                                .Element(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x)
-                                .Element(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x)
-                                .DebugPointer("Page content")
+                                .NonTrackingElement(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x)
+                                .NonTrackingElement(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x)
+                                .DebugPointer(DebugPointerType.LayoutStructure, "Page content")
                                 .Element(Content);
 
                             decoration
                                 .After()
-                                .DebugPointer("Page footer")
+                                .DebugPointer(DebugPointerType.LayoutStructure, "Page footer")
                                 .Element(Footer);
                         });
                     
                     layers
                         .Layer()
-                        .DebugPointer("Page foreground layer")
+                        .DebugPointer(DebugPointerType.LayoutStructure, "Page foreground layer")
                         .Repeat()
                         .Element(Foreground);
                 });

+ 11 - 0
Source/QuestPDF/Elements/SourceCodePointer.cs

@@ -0,0 +1,11 @@
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements;
+
+internal class SourceCodePointer : ContainerElement
+{
+    public string HandlerName { get; set; }
+    public string ParentName { get; set; }
+    public string SourceFilePath { get; set; }
+    public int SourceLineNumber { get; set; }
+}

+ 6 - 4
Source/QuestPDF/Fluent/ComponentExtentions.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Linq.Expressions;
 using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Elements;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
@@ -59,10 +60,11 @@ namespace QuestPDF.Fluent
             var descriptor = new ComponentDescriptor<T>(component);
             handler?.Invoke(descriptor);
 
-            if (System.Diagnostics.Debugger.IsAttached)
-                element = element.DebugPointer(component.GetType().Name, highlight: false);
-
-            component.Compose(element.Container());
+            var componentContainer = element
+                .Container()
+                .DebugPointer(DebugPointerType.Component, component.GetType().Name);
+            
+            component.Compose(componentContainer);
         }
         
         static void Component<T>(this IContainer element, Action<ComponentDescriptor<T>>? handler = null) where T : IComponent, new()

+ 5 - 5
Source/QuestPDF/Fluent/DebugExtensions.cs

@@ -38,17 +38,17 @@ namespace QuestPDF.Fluent
         /// This debug element does not appear in the final PDF output.
         /// </remarks>
         /// <param name="elementTraceText">Text visible somewhere in the "element trace" content identifying given document fragment.</param>
-        public static IContainer DebugPointer(this IContainer parent, string elementTraceText)
+        public static IContainer DebugPointer(this IContainer parent, string label)
         {
-            return parent.DebugPointer(elementTraceText, true);
+            return parent.DebugPointer(DebugPointerType.UserDefined, label);
         }
         
-        internal static IContainer DebugPointer(this IContainer parent, string elementTraceText, bool highlight)
+        internal static IContainer DebugPointer(this IContainer parent, DebugPointerType type, string label)
         {
             return parent.Element(new DebugPointer
             {
-                Target = elementTraceText,
-                Highlight = highlight
+                Type = type,
+                Label = label
             });
         }
     }

+ 54 - 4
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -1,4 +1,6 @@
 using System;
+using System.Net.Mime;
+using System.Runtime.CompilerServices;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Elements;
 using QuestPDF.Helpers;
@@ -46,9 +48,31 @@ namespace QuestPDF.Fluent
         /// <para>Extracting implementation of certain layout structures into separate methods, allows you to accurately describe their purpose and reuse them code in various parts of the application.</para>
         /// </remarks>
         /// <param name="handler">A delegate that takes the current container and populates it with content.</param>
-        public static void Element(this IContainer parent, Action<IContainer> handler)
+        public static void Element(
+            this IContainer parent, 
+            Action<IContainer> handler, 
+#if NETCOREAPP3_0_OR_GREATER
+            [CallerArgumentExpression("handler")] string handlerName = null,
+#endif
+            [CallerMemberName] string parentName = "",
+            [CallerFilePath] string sourceFilePath = "",
+            [CallerLineNumber] int sourceLineNumber = 0)
         {
-            handler(parent.Container());
+#if !NETCOREAPP3_0_OR_GREATER
+            const string handlerName = "Unknown function";
+#endif
+            
+            var handlerContainer = parent
+                .Container()
+                .Element(new SourceCodePointer
+                {
+                    HandlerName = handlerName,
+                    ParentName = parentName,
+                    SourceFilePath = sourceFilePath,
+                    SourceLineNumber = sourceLineNumber
+                });
+            
+            handler(handlerContainer);
         }
         
         /// <summary>
@@ -61,9 +85,35 @@ namespace QuestPDF.Fluent
         /// </remarks>
         /// <param name="handler">A method that accepts the current container, optionally populates it with content, and returns a subsequent container to continue the Fluent API chain.</param>
         /// <returns>The container returned by the <paramref name="handler"/> method.</returns>
-        public static IContainer Element(this IContainer parent, Func<IContainer, IContainer> handler)
+        public static IContainer Element(
+            this IContainer parent, 
+            Func<IContainer, IContainer> handler,
+#if NETCOREAPP3_0_OR_GREATER
+            [CallerArgumentExpression("handler")] string handlerName = null,
+#endif
+            [CallerMemberName] string parentName = "",
+            [CallerFilePath] string sourceFilePath = "",
+            [CallerLineNumber] int sourceLineNumber = 0)
+        {
+#if !NETCOREAPP3_0_OR_GREATER
+            const string handlerName = "Unknown function";
+#endif
+            
+            var handlerContainer = parent
+                .Element(new SourceCodePointer
+                {
+                    HandlerName = handlerName,
+                    ParentName = parentName,
+                    SourceFilePath = sourceFilePath,
+                    SourceLineNumber = sourceLineNumber
+                });
+            
+            return handler(handlerContainer);
+        }
+        
+        internal static IContainer NonTrackingElement(this IContainer parent, Func<IContainer, IContainer> handler)
         {
-            return handler(parent.Container()).Container();
+            return handler(parent.Container());
         }
         
         /// <summary>