Browse Source

Prototype implementation

MarcinZiabek 3 years ago
parent
commit
100afbdc32

+ 1 - 1
QuestPDF.Examples/QuestPDF.Examples.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
     <PropertyGroup>
     <PropertyGroup>
-        <TargetFramework>netcoreapp3.1</TargetFramework>
+        <TargetFramework>net6.0</TargetFramework>
         <IsPackable>false</IsPackable>
         <IsPackable>false</IsPackable>
     </PropertyGroup>
     </PropertyGroup>
 
 

+ 5 - 5
QuestPDF.Previewer.Examples/Program.cs

@@ -7,11 +7,11 @@ using QuestPDF.ReportSample.Layouts;
 
 
 //ImagePlaceholder.Solid = true;
 //ImagePlaceholder.Solid = true;
 
 
-// var model = DataSource.GetReport();
-// var report = new StandardReport(model);
-// report.ShowInPreviewer();
-//
-// return;
+var model = DataSource.GetReport();
+var report = new StandardReport(model);
+report.ShowInPreviewer();
+
+return;
 
 
 Document
 Document
     .Create(container =>
     .Create(container =>

+ 150 - 15
QuestPDF.Previewer/InteractiveCanvas.cs

@@ -2,6 +2,7 @@
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Skia;
 using Avalonia.Skia;
+using DynamicData;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace QuestPDF.Previewer;
 namespace QuestPDF.Previewer;
@@ -10,9 +11,10 @@ class InteractiveCanvas : ICustomDrawOperation
 {
 {
     public Rect Bounds { get; set; }
     public Rect Bounds { get; set; }
     public ICollection<PreviewPage> Pages { get; set; }
     public ICollection<PreviewPage> Pages { get; set; }
+    public InspectionElement? InspectionElement { get; set; }
 
 
-    private float Width => (float)Bounds.Width;
-    private float Height => (float)Bounds.Height;
+    private float ViewportWidth => (float)Bounds.Width;
+    private float ViewportHeight => (float)Bounds.Height;
 
 
     public float Scale { get; private set; } = 1;
     public float Scale { get; private set; } = 1;
     public float TranslateX { get; set; }
     public float TranslateX { get; set; }
@@ -28,7 +30,7 @@ class InteractiveCanvas : ICustomDrawOperation
     public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
     public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
     public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
     public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
     
     
-    public float MaxTranslateY => TotalHeight - Height / Scale;
+    public float MaxTranslateY => TotalHeight - ViewportHeight / Scale;
 
 
     public float ScrollPercentY
     public float ScrollPercentY
     {
     {
@@ -46,7 +48,7 @@ class InteractiveCanvas : ICustomDrawOperation
     {
     {
         get
         get
         {
         {
-            var viewPortSize = Height / Scale / TotalHeight;
+            var viewPortSize = ViewportHeight / Scale / TotalHeight;
             return Math.Clamp(viewPortSize, 0, 1);
             return Math.Clamp(viewPortSize, 0, 1);
         }
         }
     }
     }
@@ -61,19 +63,19 @@ class InteractiveCanvas : ICustomDrawOperation
     
     
     private void LimitTranslate()
     private void LimitTranslate()
     {
     {
-        if (TotalPagesHeight > Height / Scale)
+        if (TotalPagesHeight > ViewportHeight / Scale)
         {
         {
             TranslateY = Math.Min(TranslateY, MaxTranslateY);
             TranslateY = Math.Min(TranslateY, MaxTranslateY);
             TranslateY = Math.Max(TranslateY, 0);
             TranslateY = Math.Max(TranslateY, 0);
         }
         }
         else
         else
         {
         {
-            TranslateY = (TotalPagesHeight - Height / Scale) / 2;
+            TranslateY = (TotalPagesHeight - ViewportHeight / Scale) / 2;
         }
         }
 
 
-        if (Width / Scale < MaxWidth)
+        if (ViewportWidth / Scale < MaxWidth)
         {
         {
-            var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2;
+            var maxTranslateX = (ViewportWidth / 2 - SafeZone) / Scale - MaxWidth / 2;
 
 
             TranslateX = Math.Min(TranslateX, -maxTranslateX);
             TranslateX = Math.Min(TranslateX, -maxTranslateX);
             TranslateX = Math.Max(TranslateX, maxTranslateX);
             TranslateX = Math.Max(TranslateX, maxTranslateX);
@@ -104,6 +106,72 @@ class InteractiveCanvas : ICustomDrawOperation
 
 
         LimitTranslate();
         LimitTranslate();
     }
     }
+
+    public int ActivePage { get; set; } = 1;
+
+    public IEnumerable<(int pageNumber, float beginY, float endY)> GetPagePosition()
+    {
+        var pageNumber = 1;
+        var currentPagePosition = SafeZone / Scale;
+        
+        foreach (var page in Pages)
+        {
+            yield return (pageNumber, currentPagePosition, currentPagePosition + page.Height);
+            currentPagePosition += page.Height + PageSpacing;
+            pageNumber++;
+        }
+    }
+
+    public void SetActivePage(float x, float y)
+    {
+        y /= Scale;
+        y += TranslateY;
+
+        ActivePage = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY).pageNumber;
+    }
+
+    public void ScrollToInspectionElement(InspectionElement element)
+    {
+        var location = element.Location.MinBy(x => x.PageNumber);
+        var pagePosition = GetPagePosition().ElementAt(location.PageNumber - 1);
+        var page = Pages.ElementAt(location.PageNumber - 1);
+
+        var widthScale = ViewportWidth / location.Width;
+        var heightScale = ViewportHeight / location.Height;
+        var targetScale = Math.Min(widthScale, heightScale);
+        targetScale *= 0.7f; // slightly zoom out to show entire element with padding
+
+        Scale = targetScale; 
+        
+        TranslateY = pagePosition.beginY + location.Top + location.Height / 2 - ViewportHeight / Scale / 2;
+        TranslateX = page.Width / 2 - location.Left - location.Width / 2;
+    }
+    
+    public (int pageNumber, float x, float y)? FindClickedPointOnThePage(float x, float y)
+    {
+        x -= ViewportWidth / 2;
+        x /= Scale;
+        x += TranslateX;
+        
+        y /= Scale;
+        y += TranslateY;
+        
+        var location = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY);
+
+        if (location == default)
+            return null;
+
+        var page = Pages.ElementAt(location.pageNumber - 1);
+        
+        x += page.Width / 2;
+
+        if (x < 0 || page.Width < x)
+            return null;
+        
+        y -= location.beginY;
+
+        return (location.pageNumber, x, y);
+    }
     
     
     #endregion
     #endregion
     
     
@@ -124,20 +192,32 @@ class InteractiveCanvas : ICustomDrawOperation
 
 
         var originalMatrix = canvas.TotalMatrix;
         var originalMatrix = canvas.TotalMatrix;
 
 
-        canvas.Translate(Width / 2, 0);
-        
+        canvas.Translate(ViewportWidth / 2, 0);
         canvas.Scale(Scale);
         canvas.Scale(Scale);
-        canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
+        canvas.Translate(TranslateX, -TranslateY);
+
+        var topMatrix = canvas.TotalMatrix;;
+
+        var positions = GetPagePosition().ToList();
         
         
-        foreach (var page in Pages)
+        foreach (var pageIndex in Enumerable.Range(0, Pages.Count))
         {
         {
-            canvas.Translate(-page.Width / 2f, 0);
+            canvas.SetMatrix(topMatrix);
+            
+            var page = Pages.ElementAt(pageIndex);
+            var position = positions.ElementAt(pageIndex);
+            
+            canvas.Translate(-page.Width / 2f, position.beginY);
             DrawBlankPage(canvas, page.Width, page.Height);
             DrawBlankPage(canvas, page.Width, page.Height);
             canvas.DrawPicture(page.Picture);
             canvas.DrawPicture(page.Picture);
-            canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
+            DrawInspectionElement(canvas, pageIndex + 1);
         }
         }
 
 
+        canvas.SetMatrix(topMatrix);
+        DrawActivePage(canvas);
+        
         canvas.SetMatrix(originalMatrix);
         canvas.SetMatrix(originalMatrix);
+        
         DrawInnerGradient(canvas);
         DrawInnerGradient(canvas);
     }
     }
     
     
@@ -195,7 +275,62 @@ class InteractiveCanvas : ICustomDrawOperation
                 SKShaderTileMode.Clamp)
                 SKShaderTileMode.Clamp)
         };
         };
         
         
-        canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
+        canvas.DrawRect(0, 0, ViewportWidth, InnerGradientSize, fogPaint);
+    }
+
+    #endregion
+
+    #region Interactivity
+
+    private void DrawActivePage(SKCanvas canvas)
+    {
+        if (ActivePage == default)
+            return;
+        
+        var page = Pages.ElementAt(ActivePage - 1);
+        var pagePosition = GetPagePosition().ElementAt(ActivePage - 1);
+
+        var thickness = 6f / Scale;
+
+        using var strokePaint = new SKPaint
+        {
+            StrokeWidth = thickness,
+            IsStroke = true,
+            Color = SKColor.Parse("#000")
+        };
+        
+        canvas.DrawRect(- page.Width / 2 -thickness / 2, pagePosition.beginY -thickness / 2, page.Width + thickness, page.Height + thickness, strokePaint);
+    }
+    
+    private void DrawInspectionElement(SKCanvas canvas, int pageNumber)
+    {
+        if (InspectionElement == null || InspectionElement.Location == null)
+            return;
+
+        var location = InspectionElement.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
+
+        if (location == null)
+            return;
+
+        var size = 4 / Scale;
+        size = Math.Min(size, 2);
+
+        using var strokePaint = new SKPaint
+        {
+            StrokeWidth = size,
+            IsStroke = true,
+            PathEffect = SKPathEffect.CreateDash(new[] { size * 4, size * 2 }, 0),
+            Color = SKColor.Parse("#444"),
+            StrokeJoin = SKStrokeJoin.Round
+        };
+        
+        using var backgroundPaint = new SKPaint
+        {
+            Color = SKColor.Parse("#4444"),
+        };
+        
+        canvas.DrawRect(location.Left, location.Top, location.Width, location.Height, backgroundPaint);
+        canvas.DrawRect(location.Left + size / 2, location.Top + size / 2, location.Width - size, location.Height - size, strokePaint);
     }
     }
 
 
     #endregion
     #endregion

+ 81 - 0
QuestPDF.Previewer/PreviewerControl.cs

@@ -35,6 +35,22 @@ namespace QuestPDF.Previewer
             set => SetValue(ScrollViewportSizeProperty, value);
             set => SetValue(ScrollViewportSizeProperty, value);
         }
         }
         
         
+        public static readonly StyledProperty<ObservableCollection<InspectionElement>> HierarchyProperty = AvaloniaProperty.Register<PreviewerControl, ObservableCollection<InspectionElement>>(nameof(Hierarchy));
+        
+        public ObservableCollection<InspectionElement> Hierarchy
+        {
+            get => GetValue(HierarchyProperty);
+            set => SetValue(HierarchyProperty, value);
+        }
+        
+        public static readonly StyledProperty<InspectionElement> CurrentSelectionProperty = AvaloniaProperty.Register<PreviewerControl, InspectionElement>(nameof(CurrentSelection));
+        
+        public InspectionElement CurrentSelection
+        {
+            get => GetValue(CurrentSelectionProperty);
+            set => SetValue(CurrentSelectionProperty, value);
+        }
+        
         public PreviewerControl()
         public PreviewerControl()
         {
         {
             PagesProperty.Changed.Subscribe(x =>
             PagesProperty.Changed.Subscribe(x =>
@@ -49,7 +65,72 @@ namespace QuestPDF.Previewer
                 InvalidateVisual();
                 InvalidateVisual();
             });
             });
 
 
+            CurrentSelectionProperty.Changed.Subscribe(x =>
+            {
+                InteractiveCanvas.InspectionElement = CurrentSelection;
+                //InteractiveCanvas.ScrollToInspectionElement(CurrentSelection);
+                InvalidateVisual();
+            });
+            
             ClipToBounds = true;
             ClipToBounds = true;
+
+            PointerPressed += (sender, args) =>
+            {
+                var position = args.GetPosition(this);
+                InteractiveCanvas.SetActivePage((float)position.X, (float)position.Y);
+
+                var clickedPosition = InteractiveCanvas.FindClickedPointOnThePage((float)position.X, (float)position.Y);
+                
+                if (clickedPosition != null) 
+                    FindHighlightedElement(clickedPosition.Value.pageNumber, clickedPosition.Value.x, clickedPosition.Value.y);
+                
+                InvalidateVisual();
+            };
+        }
+
+        void FindHighlightedElement(int pageNumber, float x, float y)
+        {
+            var possible = FlattenHierarchy(Hierarchy.First(), 0)
+                .Select(x =>
+                {
+                    var location = x.element.Location.First(y => y.PageNumber == pageNumber);
+
+                    return new
+                    {
+                        Element = x.element,
+                        Level = x.level,
+                        Size = location.Width * location.Height
+                    };
+                })
+                .ToList();
+
+            var minSize = possible.Min(x => x.Size);
+
+            CurrentSelection = possible
+                .Where(x => Math.Abs(x.Size - minSize) < 1)
+                .OrderByDescending(x => x.Level)
+                .First()
+                .Element;
+
+            IEnumerable<(InspectionElement element, int level)> FlattenHierarchy(InspectionElement element, int level)
+            {
+                var location = element.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
+                
+                if (location == null)
+                    yield break;
+
+                if (x < location.Left || location.Left + location.Width < x)
+                    yield break;
+
+                if (y < location.Top || location.Top + location.Height < y)
+                    yield break;
+                
+                yield return (element, level);
+                
+                foreach (var childIndex in Enumerable.Range(0, element.Children.Count))
+                foreach (var nestedChild in FlattenHierarchy(element.Children[childIndex], level + childIndex + 1))
+                    yield return nestedChild;
+            }
         }
         }
         
         
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)

+ 96 - 29
QuestPDF.Previewer/PreviewerWindow.axaml

@@ -19,13 +19,17 @@
 		<Style Selector="Button.actions">
 		<Style Selector="Button.actions">
 			<Setter Property="VerticalAlignment" Value="Bottom"/>
 			<Setter Property="VerticalAlignment" Value="Bottom"/>
 			<Setter Property="HorizontalAlignment" Value="Left"/>
 			<Setter Property="HorizontalAlignment" Value="Left"/>
-			<Setter Property="Padding" Value="10"/>
-			<Setter Property="CornerRadius" Value="100"/>
-			<Setter Property="Background" Value="#888"/>
+			<Setter Property="Padding" Value="12" />
+			<Setter Property="Margin" Value="-1" />
+            <Setter Property="Background" Value="transparent"/>
 		</Style>
 		</Style>
 		
 		
+        <Style Selector="Button.active">
+            <Setter Property="Background" Value="#555"/>
+        </Style>
+        
 		<Style Selector="Button:pointerover /template/ ContentPresenter">
 		<Style Selector="Button:pointerover /template/ ContentPresenter">
-			<Setter Property="Background" Value="#999"/>
+			<Setter Property="Background" Value="#333"/>
 		</Style>
 		</Style>
 	</Window.Styles>
 	</Window.Styles>
 
 
@@ -37,29 +41,44 @@
 			</Grid.RowDefinitions>
 			</Grid.RowDefinitions>
 
 
 			<Grid.ColumnDefinitions>
 			<Grid.ColumnDefinitions>
+				<ColumnDefinition Width="Auto" />
+				<ColumnDefinition MinWidth="300" MaxWidth="500" Width="300" />
+				<ColumnDefinition Width="4" />
 				<ColumnDefinition Width="*" />
 				<ColumnDefinition Width="*" />
 				<ColumnDefinition Width="Auto" />
 				<ColumnDefinition Width="Auto" />
 			</Grid.ColumnDefinitions>
 			</Grid.ColumnDefinitions>
-			
-			<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
-			           VerticalAlignment="Center" HorizontalAlignment="Center" 
-			           TextAlignment="Center" Text="QuestPDF Previewer" FontSize="14" Foreground="#DFFF" FontWeight="Regular" IsHitTestVisible="False" />
-			
-			<previewer:PreviewerControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
-			                            HorizontalAlignment="Stretch" 
-			                            VerticalAlignment="Stretch"
-			                            CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
-			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
-			                            Pages="{Binding Pages, Mode=OneWay}" />
-			
-			<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
-				<Button Classes="actions"
-				        Command="{Binding ShowPdfCommand, Mode=OneTime}"
-				        IsVisible="{Binding !!Pages.Count}"
-				        ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
+            
+            <TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2"
+                       VerticalAlignment="Center" HorizontalAlignment="Center" 
+                       TextAlignment="Center" Text="QuestPDF Previewer" FontSize="14" Foreground="#DFFF" FontWeight="Regular" IsHitTestVisible="False" />
+
+            <StackPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Stretch" Background="#444">
+                <Button Classes="actions active"
+                        Command="{Binding ShowPdfCommand, Mode=OneTime}"
+                        ToolTip.Tip="Inspect document elements">
+                    <Viewbox Width="24" Height="24">
+                        <Canvas Width="24" Height="24">
+                            <Path Fill="White" Data="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M3.05,13H1V11H3.05C3.5,6.83 6.83,3.5 11,3.05V1H13V3.05C17.17,3.5 20.5,6.83 20.95,11H23V13H20.95C20.5,17.17 17.17,20.5 13,20.95V23H11V20.95C6.83,20.5 3.5,17.17 3.05,13M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z" />
+                        </Canvas>
+                    </Viewbox>
+                </Button>
+                
+                <Button Classes="actions"
+                        Command="{Binding ShowPdfCommand, Mode=OneTime}"
+                        ToolTip.Tip="Inspect page content">
+                    <Viewbox Width="24" Height="24">
+                        <Canvas Width="24" Height="24">
+                            <Path Fill="White" Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H13C12.59,21.75 12.2,21.44 11.86,21.1C11.53,20.77 11.25,20.4 11,20H6V4H13V9H18V10.18C18.71,10.34 19.39,10.61 20,11V8L14,2M20.31,18.9C21.64,16.79 21,14 18.91,12.68C16.8,11.35 14,12 12.69,14.08C11.35,16.19 12,18.97 14.09,20.3C15.55,21.23 17.41,21.23 18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z" />
+                        </Canvas>
+                    </Viewbox>
+                </Button>
+                
+                <Button Classes="actions"
+                        Command="{Binding ShowPdfCommand, Mode=OneTime}"
+                        ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
 					<Viewbox Width="24" Height="24">
 					<Viewbox Width="24" Height="24">
 						<Canvas Width="24" Height="24">
 						<Canvas Width="24" Height="24">
-							<Path Fill="White" Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H13C12.59,21.75 12.2,21.44 11.86,21.1C11.53,20.77 11.25,20.4 11,20H6V4H13V9H18V10.18C18.71,10.34 19.39,10.61 20,11V8L14,2M20.31,18.9C21.64,16.79 21,14 18.91,12.68C16.8,11.35 14,12 12.69,14.08C11.35,16.19 12,18.97 14.09,20.3C15.55,21.23 17.41,21.23 18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z" />
+							<Path Fill="White" Data="M12,10L8,14H11V20H13V14H16M19,4H5C3.89,4 3,4.9 3,6V18A2,2 0 0,0 5,20H9V18H5V8H19V18H15V20H19A2,2 0 0,0 21,18V6A2,2 0 0,0 19,4Z" />
 						</Canvas>
 						</Canvas>
 					</Viewbox>
 					</Viewbox>
 				</Button>
 				</Button>
@@ -84,14 +103,62 @@
 					</Viewbox>
 					</Viewbox>
 				</Button>
 				</Button>
 			</StackPanel>
 			</StackPanel>
+            
+            <Grid Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Background="#555">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="*" />
+                    <RowDefinition Height="Auto" />
+                </Grid.RowDefinitions>
+                
+                <Panel Grid.Row="0" Background="#555">
+                    <TreeView Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" SelectionMode="Single"
+                              HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0,4" AutoScrollToSelectedItem="True">
+                        <TreeView.ItemTemplate>
+                            <TreeDataTemplate ItemsSource="{Binding Children}">
+                                <TextBlock Text="{Binding Text}" Foreground="{Binding FontColor}" />
+                            </TreeDataTemplate>
+                        </TreeView.ItemTemplate>
+                    </TreeView>
+                </Panel>
+                
+                <Panel Grid.Row="1" Background="#333">
+                    <ItemsRepeater Items="{Binding SelectedItem.Metadata}" Margin="4">
+                        <ItemsRepeater.ItemTemplate>
+                            <DataTemplate DataType="previewer:Metadata">
+                                <Grid Margin="4">
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="100" />
+                                        <ColumnDefinition Width="16" />
+                                        <ColumnDefinition Width="*" />
+                                    </Grid.ColumnDefinitions>
+                                    
+                                    <TextBlock Grid.Column="0" Text="{Binding Label}" />
+                                    <TextBlock Grid.Column="2" Text="{Binding Value}" />
+                                </Grid>
+                            </DataTemplate>
+                        </ItemsRepeater.ItemTemplate>
+                    </ItemsRepeater>
+                </Panel>
+            </Grid>
+            
+            <GridSplitter Grid.Column="2" Grid.RowSpan="2" Background="#666" ResizeDirection="Columns" />
+            
+			<previewer:PreviewerControl Grid.Row="1" Grid.Column="3" Grid.ColumnSpan="2"
+			                            HorizontalAlignment="Stretch" 
+			                            VerticalAlignment="Stretch"
+			                            CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
+			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
+                                        CurrentSelection="{Binding SelectedItem, Mode=TwoWay}"
+                                        Hierarchy="{Binding Items}"
+			                            Pages="{Binding Pages, Mode=OneWay}" />
 			
 			
-			<ScrollBar Grid.Row="1" Grid.Column="1"
-			           Orientation="Vertical" 
-			           AllowAutoHide="False" 
-			           Minimum="0" Maximum="1" 
-			           IsVisible="{Binding VerticalScrollbarVisible, Mode=OneWay}"
-			           Value="{Binding CurrentScroll, Mode=TwoWay}" 
-			           ViewportSize="{Binding ScrollViewportSize, Mode=OneWay}"></ScrollBar>
+            <ScrollBar Grid.Row="1" Grid.Column="4"
+                       Orientation="Vertical" 
+                       AllowAutoHide="False" 
+                       Minimum="0" Maximum="1" 
+                       IsVisible="{Binding VerticalScrollbarVisible, Mode=OneWay}"
+                       Value="{Binding CurrentScroll, Mode=TwoWay}" 
+                       ViewportSize="{Binding ScrollViewportSize, Mode=OneWay}"></ScrollBar>
 		</Grid>
 		</Grid>
 	</Panel>
 	</Panel>
 </Window>
 </Window>

+ 70 - 0
QuestPDF.Previewer/PreviewerWindowViewModel.cs

@@ -1,5 +1,7 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using System.Diagnostics;
 using System.Diagnostics;
+using System.Text.Json;
+using Avalonia;
 using ReactiveUI;
 using ReactiveUI;
 using Unit = System.Reactive.Unit;
 using Unit = System.Reactive.Unit;
 using Avalonia.Threading;
 using Avalonia.Threading;
@@ -51,6 +53,8 @@ namespace QuestPDF.Previewer
             ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
             ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
             ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
             ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
             SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
             SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
+
+            LoadItems();
         }
         }
 
 
         private void ShowPdf()
         private void ShowPdf()
@@ -85,5 +89,71 @@ namespace QuestPDF.Previewer
             foreach (var page in oldPages)
             foreach (var page in oldPages)
                 page.Picture.Dispose();
                 page.Picture.Dispose();
         }
         }
+        
+        public ObservableCollection<InspectionElement> Items { get; set; }
+        
+        private InspectionElement _selectedItem;
+        public InspectionElement SelectedItem
+        {
+            get => _selectedItem;
+            set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
+        }
+        
+        public void LoadItems()
+        {
+            Items = new ObservableCollection<InspectionElement>();
+
+            var text = File.ReadAllText("hierarchy.json");
+            var hierarchy = JsonSerializer.Deserialize<InspectionElement>(text);
+
+            Items.Add(hierarchy);
+        }
+    }
+
+    internal class InspectionElementLocation
+    {
+        public int PageNumber { get; set; }
+        public float Top { get; set; }
+        public float Left { get; set; }
+        public float Width { get; set; }
+        public float Height { get; set; }
+    }
+
+    internal class Metadata
+    {
+        public string Label { get; set; }
+        public string Value { get; set; }
+
+        public Metadata(string label, string value)
+        {
+            Label = label;
+            Value = value;
+        }
+    }
+    
+    internal class InspectionElement
+    {
+        public string Element { get; set; }
+        public List<InspectionElementLocation> Location { get; set; }
+        public Dictionary<string, string> Properties { get; set; }
+        public List<InspectionElement> Children { get; set; }
+        public Thickness Margin { get; set; }
+        
+        public string FontColor => Element == "DebugPointer" ? "#FFF" : "#AFFF";
+        public string Text => Element == "DebugPointer" ? Properties.First(x => x.Key == "Target").Value : Element;
+
+        public IList<Metadata> Metadata => ListMetadata().ToList();
+
+        public IEnumerable<Metadata> ListMetadata()
+        {
+            yield return new Metadata("Element name", Element);
+            yield return new Metadata("Position left", Location[0].Left.ToString("N2"));
+            yield return new Metadata("Position top", Location[0].Top.ToString("N2"));
+            yield return new Metadata("Width", Location[0].Width.ToString("N2"));
+            yield return new Metadata("Height", Location[0].Height.ToString("N2"));
+
+            foreach (var property in Properties)
+                yield return new Metadata(property.Key, property.Value);
+        }
     }
     }
 }
 }

+ 3 - 0
QuestPDF.Previewer/QuestPDF.Previewer.csproj

@@ -38,6 +38,9 @@
             <Visible>false</Visible>
             <Visible>false</Visible>
             <PackagePath>\</PackagePath>
             <PackagePath>\</PackagePath>
         </None>
         </None>
+        <None Update="hierarchy.json">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
     </ItemGroup>
     </ItemGroup>
     
     
     <ItemGroup>
     <ItemGroup>

File diff suppressed because it is too large
+ 0 - 0
QuestPDF.Previewer/hierarchy.json


+ 222 - 1
QuestPDF/Drawing/DocumentGenerator.cs

@@ -1,7 +1,9 @@
 using System;
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Text;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
@@ -10,6 +12,8 @@ using QuestPDF.Elements.Text.Items;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using System.Text.Json;
+using System.Text.Json.Serialization;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
@@ -75,7 +79,216 @@ namespace QuestPDF.Drawing
                 ApplyCaching(content);
                 ApplyCaching(content);
 
 
             RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
             RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
+            
+            if (metadata.ApplyInspection)
+                ApplyInspection(content);
+            
             RenderPass(pageContext, canvas, content, metadata, debuggingState);
             RenderPass(pageContext, canvas, content, metadata, debuggingState);
+
+            if (metadata.ApplyInspection)
+            {
+                var x = TraverseStatisticsToJson(content);
+                var y = JsonSerializer.Serialize(x);   
+            }
+        }
+
+        internal static string TraverseStructure(Element container)
+        {
+            var builder = new StringBuilder();
+            var nestingLevel = 0;
+
+            Traverse(container);
+            return builder.ToString();
+
+            void Traverse(Element item)
+            {
+                var indent = new string(' ', nestingLevel * 4);
+                var title = item.GetType().Name;
+                builder.AppendLine(indent + title);
+
+                nestingLevel++;
+                
+                foreach (var child in item.GetChildren())
+                    Traverse(child);
+                
+                nestingLevel--;
+            }
+        }
+
+        internal static string TraverseStatistics(Element container, int pageNumber)
+        {
+            var builder = new StringBuilder();
+            var nestingLevel = 0;
+
+            Traverse(container);
+            return builder.ToString();
+
+            void Traverse(Element item)
+            {
+                if (item is DebuggingProxy or CacheProxy or Container)
+                {
+                    Traverse(item.GetChildren().First());
+                    return;
+                }
+                
+                var inspectionItem = item as InspectionProxy;
+
+                if (inspectionItem == null)
+                {
+                    var children = item.GetChildren().ToList();
+                        
+                    if (children.Count > 1)
+                        nestingLevel++;
+                    
+                    children.ForEach(Traverse);
+                    
+                    if (children.Count > 1)
+                        nestingLevel--;
+                    
+                    return;
+                }
+
+                if (!inspectionItem.Statistics.ContainsKey(pageNumber))
+                    return;
+                
+                var statistics = inspectionItem.Statistics[pageNumber];
+                
+                var indent = new string(' ', nestingLevel * 4);
+                var title = statistics.Element.GetType().Name;
+                
+                builder.AppendLine(indent + title);
+                builder.AppendLine(indent + new string('-', title.Length));
+                
+                builder.AppendLine(indent + "Size: " + statistics.Size);
+                builder.AppendLine(indent + "Position: " + statistics.Position);
+                
+                foreach (var configuration in DebuggingState.GetElementConfiguration(statistics.Element))
+                    builder.AppendLine(indent + configuration);
+                
+                builder.AppendLine();
+                
+                Traverse(inspectionItem.Child);
+            }
+        }
+        
+        internal static InspectionElement TraverseStatisticsToJson(Element container)
+        {
+            return Traverse(container);
+            
+            InspectionElement? Traverse(Element item)
+            {
+                InspectionElement? result = null;
+                Element currentItem = item;
+                
+                while (true)
+                {
+                    if (currentItem is InspectionProxy proxy)
+                    {
+                        if (proxy.Child.GetType() == typeof(Container))
+                        {
+                            currentItem = proxy.Child;
+                            continue;
+                        }
+                        
+                        var statistics = GetInspectionElement(proxy);
+
+                        if (statistics == null)
+                            return null;
+                        
+                        if (result == null)
+                        {
+                            result = statistics;
+                        }
+                        else
+                        {
+                            result.Children.Add(statistics);
+                        }
+
+                        currentItem = proxy.Child;
+                    }
+                    else
+                    {
+                        var children = currentItem.GetChildren().ToList();
+
+                        if (children.Count == 0)
+                        {
+                            return result;
+                        }
+                        else if (children.Count == 1)
+                        {
+                            currentItem = children.First();
+                            continue;
+                        }
+                        else
+                        {
+                            children
+                                .Select(Traverse)
+                                .Where(x => x != null)
+                                .ToList()
+                                .ForEach(result.Children.Add);
+
+                            return result;
+                        }
+                    }
+                }
+            }
+
+            static InspectionElement? GetInspectionElement(InspectionProxy inspectionProxy)
+            {
+                var locations = inspectionProxy
+                    .Statistics
+                    .Keys
+                    .Select(x =>
+                    {
+                        var statistics = inspectionProxy.Statistics[x];
+                        
+                        return new InspectionElementLocation
+                        {
+                            PageNumber = x,
+                            Top = statistics.Position.Y,
+                            Left = statistics.Position.X,
+                            Width = statistics.Size.Width,
+                            Height = statistics.Size.Height,
+                        };
+                    })
+                    .ToList();
+                
+                return new InspectionElement
+                {
+                    Element = inspectionProxy.Child.GetType().Name,
+                    Location = locations,
+                    Properties = GetElementConfiguration(inspectionProxy.Child),
+                    Children = new List<InspectionElement>()
+                };
+            }
+            
+            static Dictionary<string, string> GetElementConfiguration(IElement element)
+            {
+                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))
+                    .ToDictionary(x => x.Property, x => FormatValue(x.Value));
+
+                string FormatValue(object value)
+                {
+                    const int maxLength = 100;
+                    
+                    var text = value?.ToString() ?? "-";
+
+                    if (text.Length < maxLength)
+                        return text;
+
+                    return text.AsSpan(0, maxLength).ToString() + "...";
+                }
+            }
         }
         }
         
         
         internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
         internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
@@ -157,11 +370,19 @@ namespace QuestPDF.Drawing
 
 
             content.VisitChildren(x =>
             content.VisitChildren(x =>
             {
             {
-                x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
+                x.CreateProxy(y => y is ElementProxy ? y : new DebuggingProxy(debuggingState, y));
             });
             });
 
 
             return debuggingState;
             return debuggingState;
         }
         }
+        
+        private static void ApplyInspection(Container content)
+        {
+            content.VisitChildren(x =>
+            {
+                x.CreateProxy(y => y is ElementProxy ? y : new InspectionProxy(y));
+            });
+        }
 
 
         internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
         internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
         {
         {

+ 1 - 0
QuestPDF/Drawing/DocumentMetadata.cs

@@ -26,6 +26,7 @@ namespace QuestPDF.Drawing
 
 
         public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
         public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
         public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
         public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
+        public bool ApplyInspection { get; set; } = System.Diagnostics.Debugger.IsAttached;
 
 
         public static DocumentMetadata Default => new DocumentMetadata();
         public static DocumentMetadata Default => new DocumentMetadata();
     }
     }

+ 25 - 25
QuestPDF/Drawing/Proxy/DebuggingState.cs

@@ -94,36 +94,36 @@ namespace QuestPDF.Drawing.Proxy
                 item.Stack.ToList().ForEach(Traverse);
                 item.Stack.ToList().ForEach(Traverse);
                 nestingLevel--;
                 nestingLevel--;
             }
             }
-
-            static IEnumerable<string> GetElementConfiguration(IElement element)
-            {
-                if (element is DebugPointer)
-                    return Enumerable.Empty<string>();
+        }
+        
+        internal 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)
+            return element
+                .GetType()
+                .GetProperties()
+                .Select(x => new
                 {
                 {
-                    const int maxLength = 100;
+                    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() ?? "-";
+                var text = value?.ToString() ?? "-";
 
 
-                    if (text.Length < maxLength)
-                        return text;
+                if (text.Length < maxLength)
+                    return text;
 
 
-                    return text.AsSpan(0, maxLength).ToString() + "...";
-                }
+                return text.AsSpan(0, maxLength).ToString() + "...";
             }
             }
         }
         }
     }
     }

+ 35 - 0
QuestPDF/Drawing/Proxy/InspectionProxy.cs

@@ -0,0 +1,35 @@
+using System.Collections;
+using System.Collections.Generic;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing.Proxy
+{
+    internal class InspectionProxy : ElementProxy
+    {
+        public Dictionary<int, InspectionStateItem> Statistics { get; set; } = new();
+
+        public InspectionProxy(Element child)
+        {
+            Child = child;
+        }
+
+        internal override void Draw(Size availableSpace)
+        {
+            if (Canvas is SkiaCanvasBase canvas)
+            {
+                var matrix = canvas.Canvas.TotalMatrix;
+
+                var inspectionItem = new InspectionStateItem
+                {
+                    Element = Child,
+                    Position = new Position(matrix.TransX, matrix.TransY),
+                    Size = availableSpace
+                };
+
+                Statistics[PageContext.CurrentPage] = inspectionItem;
+            }
+            
+            base.Draw(availableSpace);
+        }
+    }
+}

+ 7 - 0
QuestPDF/Drawing/Proxy/InspectionState.cs

@@ -0,0 +1,7 @@
+namespace QuestPDF.Drawing.Proxy
+{
+    public class InspectionState
+    {
+        
+    }
+}

+ 29 - 0
QuestPDF/Drawing/Proxy/InspectionStateItem.cs

@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing.Proxy
+{
+    internal class InspectionStateItem
+    {
+        public Element Element { get; internal set; }
+        public Size Size { get; internal set; }
+        public Position Position { get; internal set; }
+    }
+
+    internal class InspectionElementLocation
+    {
+        public int PageNumber { get; internal set; }
+        public float Top { get; set; }
+        public float Left { get; set; }
+        public float Width { get; set; }
+        public float Height { get; set; }
+    }
+    
+    internal class InspectionElement
+    {
+        public string Element { get; internal set; }
+        public ICollection<InspectionElementLocation> Location { get; internal set; }
+        public Dictionary<string, string> Properties { get; internal set; }
+        public ICollection<InspectionElement> Children { get; set; }
+    }
+}

+ 1 - 1
QuestPDF/Elements/Layers.cs

@@ -19,7 +19,7 @@ namespace QuestPDF.Elements
         {
         {
             return Children;
             return Children;
         }
         }
-        
+
         internal override SpacePlan Measure(Size availableSpace)
         internal override SpacePlan Measure(Size availableSpace)
         {
         {
             return Children
             return Children

+ 1 - 0
QuestPDF/Elements/Page.cs

@@ -30,6 +30,7 @@ namespace QuestPDF.Elements
         public void Compose(IContainer container)
         public void Compose(IContainer container)
         {
         {
             container
             container
+                .Container()
                 .Background(BackgroundColor)
                 .Background(BackgroundColor)
                 .Layers(layers =>
                 .Layers(layers =>
                 {
                 {

+ 0 - 5
QuestPDF/Elements/Row.cs

@@ -46,11 +46,6 @@ namespace QuestPDF.Elements
             return Items;
             return Items;
         }
         }
         
         
-        internal override void CreateProxy(Func<Element?, Element?> create)
-        {
-            Items.ForEach(x => x.Child = create(x.Child));
-        }
-
         internal override SpacePlan Measure(Size availableSpace)
         internal override SpacePlan Measure(Size availableSpace)
         {
         {
             if (!Items.Any())
             if (!Items.Any())

+ 1 - 1
QuestPDF/Fluent/ColumnExtensions.cs

@@ -22,7 +22,7 @@ namespace QuestPDF.Fluent
                 Child = container
                 Child = container
             });
             });
             
             
-            return container;
+            return container.DebugPointer("Column Item");;
         }
         }
     }
     }
     
     

+ 3 - 3
QuestPDF/Fluent/DecorationExtensions.cs

@@ -12,7 +12,7 @@ namespace QuestPDF.Fluent
         {
         {
             var container = new Container();
             var container = new Container();
             Decoration.Before = container;
             Decoration.Before = container;
-            return container;
+            return container.DebugPointer("Decoration Before");
         }
         }
         
         
         public void Before(Action<IContainer> handler)
         public void Before(Action<IContainer> handler)
@@ -24,7 +24,7 @@ namespace QuestPDF.Fluent
         {
         {
             var container = new Container();
             var container = new Container();
             Decoration.Content = container;
             Decoration.Content = container;
-            return container;
+            return container.DebugPointer("Decoration Content");
         }
         }
         
         
         public void Content(Action<IContainer> handler)
         public void Content(Action<IContainer> handler)
@@ -36,7 +36,7 @@ namespace QuestPDF.Fluent
         {
         {
             var container = new Container();
             var container = new Container();
             Decoration.After = container;
             Decoration.After = container;
-            return container;
+            return container.DebugPointer("Decoration After");
         }
         }
         
         
         public void After(Action<IContainer> handler)
         public void After(Action<IContainer> handler)

+ 1 - 1
QuestPDF/Fluent/RowExtensions.cs

@@ -22,7 +22,7 @@ namespace QuestPDF.Fluent
             };
             };
             
             
             Row.Items.Add(element);
             Row.Items.Add(element);
-            return element;
+            return element.DebugPointer("Row Item");
         }
         }
         
         
         [Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")]
         [Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")]

+ 1 - 1
QuestPDF/QuestPDF.csproj

@@ -17,7 +17,7 @@
         <PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
         <PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
         <PackageLicenseExpression>MIT</PackageLicenseExpression>
         <PackageLicenseExpression>MIT</PackageLicenseExpression>
         <Nullable>enable</Nullable>
         <Nullable>enable</Nullable>
-        <TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0;net6.0</TargetFrameworks>
+        <TargetFramework>net6.0</TargetFramework>
         <IncludeSymbols>true</IncludeSymbols>
         <IncludeSymbols>true</IncludeSymbols>
         <SymbolPackageFormat>snupkg</SymbolPackageFormat>
         <SymbolPackageFormat>snupkg</SymbolPackageFormat>
     </PropertyGroup>
     </PropertyGroup>

Some files were not shown because too many files changed in this diff