diff --git a/QuestPDF.Previewer.Examples/Program.cs b/QuestPDF.Previewer.Examples/Program.cs
index 96e7c10..7e218a8 100644
--- a/QuestPDF.Previewer.Examples/Program.cs
+++ b/QuestPDF.Previewer.Examples/Program.cs
@@ -3,8 +3,14 @@ using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
+using QuestPDF.ReportSample;
+using QuestPDF.ReportSample.Layouts;
using Colors = QuestPDF.Helpers.Colors;
+var model = DataSource.GetReport();
+var report = new StandardReport(model);
+report.ShowInPreviewer();
+
Document
.Create(container =>
{
@@ -53,9 +59,9 @@ Document
container.Page(page =>
{
- page.Size(PageSizes.A3);
+ page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
- page.PageColor(Colors.White);
+ page.PageColor(Colors.Red.Medium);
page.DefaultTextStyle(x => x.FontSize(20));
page.Content()
diff --git a/QuestPDF.Previewer.Examples/QuestPDF.Previewer.Examples.csproj b/QuestPDF.Previewer.Examples/QuestPDF.Previewer.Examples.csproj
index e10330b..bda9681 100644
--- a/QuestPDF.Previewer.Examples/QuestPDF.Previewer.Examples.csproj
+++ b/QuestPDF.Previewer.Examples/QuestPDF.Previewer.Examples.csproj
@@ -9,6 +9,7 @@
+
diff --git a/QuestPDF.Previewer/FluentWindow.cs b/QuestPDF.Previewer/FluentWindow.cs
deleted file mode 100644
index de63e30..0000000
--- a/QuestPDF.Previewer/FluentWindow.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Platform;
-using Avalonia.Styling;
-
-namespace QuestPDF.Previewer
-{
- internal class FluentWindow : Window, IStyleable
- {
- Type IStyleable.StyleKey => typeof(Window);
-
- public FluentWindow()
- {
- ExtendClientAreaToDecorationsHint = true;
- ExtendClientAreaTitleBarHeightHint = -1;
-
- TransparencyLevelHint = WindowTransparencyLevel.AcrylicBlur;
-
- this.GetObservable(WindowStateProperty)
- .Subscribe(x =>
- {
- PseudoClasses.Set(":maximized", x == WindowState.Maximized);
- PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
- });
-
- this.GetObservable(IsExtendedIntoWindowDecorationsProperty)
- .Subscribe(x =>
- {
- if (!x)
- {
- SystemDecorations = SystemDecorations.Full;
- TransparencyLevelHint = WindowTransparencyLevel.Blur;
- }
- });
- }
-
- protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
- {
- base.OnApplyTemplate(e);
- ExtendClientAreaChromeHints =
- ExtendClientAreaChromeHints.PreferSystemChrome |
- ExtendClientAreaChromeHints.OSXThickTitleBar;
- }
- }
-}
diff --git a/QuestPDF.Previewer/InteractiveCanvas.cs b/QuestPDF.Previewer/InteractiveCanvas.cs
new file mode 100644
index 0000000..e4a7a79
--- /dev/null
+++ b/QuestPDF.Previewer/InteractiveCanvas.cs
@@ -0,0 +1,168 @@
+using System.Diagnostics;
+using Avalonia;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using QuestPDF.Helpers;
+using SkiaSharp;
+
+namespace QuestPDF.Previewer;
+
+class InteractiveCanvas : ICustomDrawOperation
+{
+ public Rect Bounds { get; set; }
+ public ICollection Pages { get; set; }
+
+ private float Width => (float)Bounds.Width;
+ private float Height => (float)Bounds.Height;
+
+ public float Scale { get; private set; } = 1;
+ public float TranslateX { get; private set; }
+ public float TranslateY { get; private set; }
+
+ private const float MinScale = 0.1f;
+ private const float MaxScale = 10f;
+
+ private const float PageSpacing = 50f;
+ private const float SafeZone = 50f;
+
+ private void LimitScale()
+ {
+ Scale = Math.Max(Scale, MinScale);
+ Scale = Math.Min(Scale, MaxScale);
+ }
+
+ private void LimitTranslate()
+ {
+ var totalHeight = Pages.Sum(x => x.Size.Height) + (Pages.Count - 1) * PageSpacing;
+ var maxWidth = Pages.Max(x => x.Size.Width);
+
+ var maxTranslateY = SafeZone / Scale;
+ var minTranslateY = (Height - SafeZone) / Scale - totalHeight;
+
+ TranslateY = Math.Min(TranslateY, maxTranslateY);
+ TranslateY = Math.Max(TranslateY, minTranslateY);
+
+ if (maxWidth < Width / Scale)
+ {
+ TranslateX = 0;
+ }
+ else
+ {
+ var maxTranslateX = (Width / 2 - SafeZone) / Scale - maxWidth / 2;
+
+ TranslateX = Math.Min(TranslateX, -maxTranslateX);
+ TranslateX = Math.Max(TranslateX, maxTranslateX);
+ }
+ }
+
+ public void TranslateWithCurrentScale(float x, float y)
+ {
+ TranslateX += x / Scale;
+ TranslateY += y / Scale;
+
+ LimitTranslate();
+ }
+
+ public void ZoomToPoint(float x, float y, float factor)
+ {
+ var oldScale = Scale;
+ Scale *= factor;
+
+ LimitScale();
+
+ factor = Scale / oldScale;
+
+ TranslateX -= x / (oldScale * factor) - x / oldScale;
+ TranslateY -= y / (oldScale * factor) - y / oldScale;
+
+ LimitTranslate();
+ }
+
+ public void Render(IDrawingContextImpl context)
+ {
+ if (Pages.Count <= 0)
+ return;
+
+ LimitScale();
+ LimitTranslate();
+
+
+ var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
+
+ if (canvas == null)
+ throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
+
+ var originalMatrix = canvas.TotalMatrix;
+
+
+ canvas.Translate(Width / 2, Height / 2);
+
+ canvas.Scale(Scale);
+ canvas.Translate(TranslateX, TranslateY);
+
+ foreach (var page in Pages)
+ {
+ canvas.Translate(-page.Size.Width / 2f, 0);
+ DrawPageShadow(canvas, page.Size);
+ canvas.DrawPicture(page.Picture);
+ canvas.Translate(page.Size.Width / 2f, page.Size.Height + PageSpacing);
+ }
+
+ canvas.SetMatrix(originalMatrix);
+
+ if (TranslateY < 0)
+ DrawInnerGradient(canvas);
+ }
+
+ public void Dispose() { }
+ public bool Equals(ICustomDrawOperation? other) => false;
+ public bool HitTest(Point p) => true;
+
+ #region page shadow
+
+ private static readonly SKImageFilter PageShadow1 = SKImageFilter.CreateDropShadowOnly(0, 6, 6, 6, SKColors.Black.WithAlpha(64));
+ private static readonly SKImageFilter PageShadow2 = SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32));
+
+ private static SKPaint PageShadowPaint = new SKPaint
+ {
+ ImageFilter = SKImageFilter.CreateBlendMode(SKBlendMode.Overlay, PageShadow1, PageShadow2)
+ };
+
+ private void DrawPageShadow(SKCanvas canvas, QuestPDF.Infrastructure.Size size)
+ {
+ canvas.DrawRect(0, 0, size.Width, size.Height, PageShadowPaint);
+ }
+
+ #endregion
+
+ #region inner viewport gradient
+
+ private const float InnerGradientSize = 24f;
+ private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
+
+ private void DrawInnerGradient(SKCanvas canvas)
+ {
+ // gamma correction
+ var colors = Enumerable
+ .Range(0, 17)
+ .Select(x => 1f - x / 16f)
+ .Select(x => Math.Pow(x, 2f))
+ .Select(x => (byte)(x * 255))
+ .Select(x => InnerGradientColor.WithAlpha(x))
+ .ToArray();
+
+ using var fogPaint = new SKPaint
+ {
+ Shader = SKShader.CreateLinearGradient(
+ new SKPoint(0, 0),
+ new SKPoint(0, InnerGradientSize),
+ colors,
+ SKShaderTileMode.Clamp)
+ };
+
+ canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
+ }
+
+ #endregion
+}
diff --git a/QuestPDF.Previewer/PreviewerApp.axaml b/QuestPDF.Previewer/PreviewerApp.axaml
index da35cb1..725db69 100644
--- a/QuestPDF.Previewer/PreviewerApp.axaml
+++ b/QuestPDF.Previewer/PreviewerApp.axaml
@@ -2,6 +2,6 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-
+
\ No newline at end of file
diff --git a/QuestPDF.Previewer/PreviewerControl.cs b/QuestPDF.Previewer/PreviewerControl.cs
new file mode 100644
index 0000000..ee98950
--- /dev/null
+++ b/QuestPDF.Previewer/PreviewerControl.cs
@@ -0,0 +1,98 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices.ComTypes;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+
+namespace QuestPDF.Previewer
+{
+ internal class PreviewerControl : Control
+ {
+ public static readonly StyledProperty?> PagesProperty =
+ AvaloniaProperty.Register>(nameof(Pages));
+
+ private InteractiveCanvas InteractiveCanvas { get; set; } = new InteractiveCanvas();
+
+ public ObservableCollection? Pages
+ {
+ get => GetValue(PagesProperty);
+ set => SetValue(PagesProperty, value);
+ }
+
+ public PreviewerControl()
+ {
+ PagesProperty.Changed.Subscribe(p =>
+ {
+ InteractiveCanvas.Pages = Pages;
+ InvalidateVisual();
+ });
+
+ ClipToBounds = true;
+ }
+
+ protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+ {
+ base.OnPointerWheelChanged(e);
+
+ if ((e.KeyModifiers & KeyModifiers.Control) != 0)
+ {
+ var scaleFactor = 1 + e.Delta.Y / 10f;
+ var point = Bounds.Center - Bounds.TopLeft - e.GetPosition(this);
+
+ InteractiveCanvas.ZoomToPoint((float)point.X, (float)point.Y, (float)scaleFactor);
+ }
+
+ if (e.KeyModifiers == KeyModifiers.None)
+ {
+ var translation = (float)e.Delta.Y * 25;
+ InteractiveCanvas.TranslateWithCurrentScale(0, translation);
+ }
+
+ InvalidateVisual();
+ }
+
+ private bool IsMousePressed { get; set; }
+ private Vector MousePosition { get; set; }
+
+ protected override void OnPointerMoved(PointerEventArgs e)
+ {
+ base.OnPointerMoved(e);
+
+ if (IsMousePressed)
+ {
+ var currentPosition = e.GetPosition(this);
+ var translation = currentPosition - MousePosition;
+ InteractiveCanvas.TranslateWithCurrentScale((float)translation.X, (float)translation.Y);
+
+ InvalidateVisual();
+ }
+
+ MousePosition = e.GetPosition(this);
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ base.OnPointerPressed(e);
+ IsMousePressed = true;
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ IsMousePressed = false;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
+
+ context.Custom(InteractiveCanvas);
+ base.Render(context);
+ }
+ }
+}
diff --git a/QuestPDF.Previewer/PreviewerPageControl.cs b/QuestPDF.Previewer/PreviewerPageControl.cs
deleted file mode 100644
index 8025182..0000000
--- a/QuestPDF.Previewer/PreviewerPageControl.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Reactive.Linq;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Media;
-
-namespace QuestPDF.Previewer
-{
- internal class PreviewerPageControl : Control
- {
- public static readonly StyledProperty PageProperty =
- AvaloniaProperty.Register(nameof(Page));
-
- public RenderedPageInfo? Page
- {
- get => GetValue(PageProperty);
- set => SetValue(PageProperty, value);
- }
-
- public PreviewerPageControl()
- {
- PageProperty.Changed.Take(1).Subscribe(p =>
- {
- var size = p.NewValue.Value?.Size ?? Infrastructure.Size.Zero;
- Width = size.Width;
- Height = size.Height;
- });
-
- ClipToBounds = true;
- }
-
- public override void Render(DrawingContext context)
- {
- base.Render(context);
-
- //context.DrawRectangle(Brushes.Red, null, new Rect(0, 0, Bounds.Width, Bounds.Height));
-
- var picture = Page?.Picture;
- if (picture != null)
- context.Custom(new SkCustomDrawOperation(
- new Rect(0, 0, Bounds.Width, Bounds.Height),
- c => c.DrawPicture(picture)));
- }
- }
-}
diff --git a/QuestPDF.Previewer/PreviewerWindow.axaml b/QuestPDF.Previewer/PreviewerWindow.axaml
index fd04a39..9e362d6 100644
--- a/QuestPDF.Previewer/PreviewerWindow.axaml
+++ b/QuestPDF.Previewer/PreviewerWindow.axaml
@@ -1,4 +1,4 @@
-
-
-
-
-
+
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/QuestPDF.Previewer/PreviewerWindow.axaml.cs b/QuestPDF.Previewer/PreviewerWindow.axaml.cs
index cfc104b..33afa93 100644
--- a/QuestPDF.Previewer/PreviewerWindow.axaml.cs
+++ b/QuestPDF.Previewer/PreviewerWindow.axaml.cs
@@ -6,7 +6,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
- internal class PreviewerWindow : FluentWindow
+ internal class PreviewerWindow : Window
{
public DocumentRenderer DocumentRenderer { get; } = new();
@@ -22,9 +22,9 @@ namespace QuestPDF.Previewer
public PreviewerWindow()
{
InitializeComponent();
-
- this.FindControl