Browse Source

Implemented basic canvas interactions

MarcinZiabek 3 years ago
parent
commit
321cba32ce

+ 8 - 2
QuestPDF.Previewer.Examples/Program.cs

@@ -3,8 +3,14 @@ using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Previewer;
 using QuestPDF.Previewer;
+using QuestPDF.ReportSample;
+using QuestPDF.ReportSample.Layouts;
 using Colors = QuestPDF.Helpers.Colors;
 using Colors = QuestPDF.Helpers.Colors;
 
 
+var model = DataSource.GetReport();
+var report = new StandardReport(model);
+report.ShowInPreviewer();
+
 Document
 Document
     .Create(container =>
     .Create(container =>
     {
     {
@@ -53,9 +59,9 @@ Document
         
         
         container.Page(page =>
         container.Page(page =>
         {
         {
-            page.Size(PageSizes.A3);
+            page.Size(PageSizes.A4);
             page.Margin(2, Unit.Centimetre);
             page.Margin(2, Unit.Centimetre);
-            page.PageColor(Colors.White);
+            page.PageColor(Colors.Red.Medium);
             page.DefaultTextStyle(x => x.FontSize(20));
             page.DefaultTextStyle(x => x.FontSize(20));
 
 
             page.Content()
             page.Content()

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

@@ -9,6 +9,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\QuestPDF.Previewer\QuestPDF.Previewer.csproj" />
     <ProjectReference Include="..\QuestPDF.Previewer\QuestPDF.Previewer.csproj" />
+    <ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
     <ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
     <ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 0 - 46
QuestPDF.Previewer/FluentWindow.cs

@@ -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;
-        }
-    }
-}

+ 168 - 0
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<RenderedPageInfo> 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
+}

+ 1 - 1
QuestPDF.Previewer/PreviewerApp.axaml

@@ -2,6 +2,6 @@
              xmlns="https://github.com/avaloniaui"
              xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 	<Application.Styles>
 	<Application.Styles>
-		<FluentTheme Mode="Dark"/>
+		<FluentTheme Mode="Dark" />
 	</Application.Styles>
 	</Application.Styles>
 </Application>
 </Application>

+ 98 - 0
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<ObservableCollection<RenderedPageInfo>?> PagesProperty =
+            AvaloniaProperty.Register<PreviewerControl, ObservableCollection<RenderedPageInfo>>(nameof(Pages));
+
+        private InteractiveCanvas InteractiveCanvas { get; set; } = new InteractiveCanvas();
+        
+        public ObservableCollection<RenderedPageInfo>? 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);
+        }
+    }
+}

+ 0 - 44
QuestPDF.Previewer/PreviewerPageControl.cs

@@ -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<RenderedPageInfo?> PageProperty =
-            AvaloniaProperty.Register<PreviewerPageControl, RenderedPageInfo?>(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)));
-        }
-    }
-}

+ 26 - 43
QuestPDF.Previewer/PreviewerWindow.axaml

@@ -1,4 +1,4 @@
-<previewer:FluentWindow xmlns="https://github.com/avaloniaui"
+<Window xmlns="https://github.com/avaloniaui"
 							xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 							xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 							xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 							xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 							xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 							xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -6,55 +6,38 @@
 							mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
 							mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
 							x:Class="QuestPDF.Previewer.PreviewerWindow"
 							x:Class="QuestPDF.Previewer.PreviewerWindow"
 							WindowStartupLocation="CenterScreen"
 							WindowStartupLocation="CenterScreen"
+							ExtendClientAreaToDecorationsHint="true"
+							ExtendClientAreaTitleBarHeightHint="-1"
+							Background="#666"
 							Icon="/Images/Logo.png"
 							Icon="/Images/Logo.png"
 							UseLayoutRounding="True"
 							UseLayoutRounding="True"
-							Background="{x:Null}"
 							x:Name="Window"
 							x:Name="Window"
 							Title="QuestPDF Document Preview">
 							Title="QuestPDF Document Preview">
-	<previewer:FluentWindow.Styles>
-		<Style Selector="TitleBar:fullscreen">
-			<Setter Property="Background" Value="#F00" />
-		</Style>
-	</previewer:FluentWindow.Styles>
-
+	
 	<Panel>
 	<Panel>
-		<ExperimentalAcrylicBorder IsHitTestVisible="False">
-			<ExperimentalAcrylicBorder.Material>
-				<ExperimentalAcrylicMaterial TintColor="Black" MaterialOpacity="0.75" TintOpacity="1" />
-			</ExperimentalAcrylicBorder.Material>
-		</ExperimentalAcrylicBorder>
-
-		<Grid Margin="10,40,10,0">
+		<Grid>
 			<Grid.RowDefinitions>
 			<Grid.RowDefinitions>
-				<RowDefinition Height="40"/>
-				<RowDefinition Height="Auto"/>
-				<RowDefinition Height="*"/>
+				<RowDefinition Height="32" />
+				<RowDefinition Height="*" />
+				<RowDefinition Height="Auto" />
 			</Grid.RowDefinitions>
 			</Grid.RowDefinitions>
 
 
-			<DockPanel LastChildFill="False" VerticalAlignment="Center" Margin="20,0">
-				<TextBlock VerticalAlignment="Center" Text="QuestPDF Document Preview" DockPanel.Dock="Left" FontWeight="SemiBold" />
-				<Button x:Name="GeneratePdf" Content="Generate PDF" DockPanel.Dock="Right" />
-			</DockPanel>
-
-			<ProgressBar IsVisible="{Binding #Window.DocumentRenderer.IsRendering}"
-									 Margin="20,0"
-									 Grid.Row="1"
-									 HorizontalAlignment="Stretch"									 
-									 Height="5"
-								   IsIndeterminate="True"/>
-
-			<ScrollViewer Grid.Row="2" VerticalAlignment="Top">
-				<ItemsRepeater HorizontalAlignment="Center" Margin="0,10" Grid.Row="1" VerticalAlignment="Top" Items="{Binding #Window.DocumentRenderer.Pages, Mode=OneWay}">
-					<ItemsRepeater.Layout>
-						<UniformGridLayout MaximumRowsOrColumns="1" Orientation="Horizontal" MinRowSpacing="25" MinColumnSpacing="25" />
-					</ItemsRepeater.Layout>
-					<ItemsRepeater.ItemTemplate>
-						<DataTemplate>
-							<previewer:PreviewerPageControl Page="{Binding}" />
-						</DataTemplate>
-					</ItemsRepeater.ItemTemplate>
-				</ItemsRepeater>
-			</ScrollViewer>
+			<Grid.ColumnDefinitions>
+				<ColumnDefinition Width="*" />
+				<ColumnDefinition Width="Auto" />
+			</Grid.ColumnDefinitions>
+			
+			<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" 
+			           VerticalAlignment="Center" HorizontalAlignment="Center" 
+			           TextAlignment="Center" Text="QuestPDF Document Preview" FontSize="14" Foreground="#DFFF" FontWeight="Regular" />
+			
+			<previewer:PreviewerControl Grid.Row="1" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2"
+			                            HorizontalAlignment="Stretch" 
+			                            VerticalAlignment="Stretch"
+			                            Pages="{Binding #Window.DocumentRenderer.Pages, Mode=OneWay}" />
+			
+			<ScrollBar Grid.Row="2" Grid.Column="0" Orientation="Horizontal" />
+			<ScrollBar Grid.Row="1" Grid.Column="1" Orientation="Vertical" />
 		</Grid>
 		</Grid>
 	</Panel>
 	</Panel>
-</previewer:FluentWindow>
+</Window>

+ 4 - 4
QuestPDF.Previewer/PreviewerWindow.axaml.cs

@@ -6,7 +6,7 @@ using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Previewer
 namespace QuestPDF.Previewer
 {
 {
-    internal class PreviewerWindow : FluentWindow
+    internal class PreviewerWindow : Window
     {
     {
         public DocumentRenderer DocumentRenderer { get; } = new();
         public DocumentRenderer DocumentRenderer { get; } = new();
 
 
@@ -22,9 +22,9 @@ namespace QuestPDF.Previewer
         public PreviewerWindow()
         public PreviewerWindow()
         {
         {
             InitializeComponent();
             InitializeComponent();
-
-            this.FindControl<Button>("GeneratePdf")
-                .Click += (_, _) => _ = PreviewerUtils.SavePdfWithDialog(Document, this);
+            //
+            // this.FindControl<Button>("GeneratePdf")
+            //     .Click += (_, _) => _ = PreviewerUtils.SavePdfWithDialog(Document, this);
 
 
             DocumentProperty.Changed.Subscribe(v => Task.Run(() => DocumentRenderer.UpdateDocument(v.NewValue.Value)));
             DocumentProperty.Changed.Subscribe(v => Task.Run(() => DocumentRenderer.UpdateDocument(v.NewValue.Value)));
             HotReloadManager.UpdateApplicationRequested += (_, _) => InvalidatePreview();
             HotReloadManager.UpdateApplicationRequested += (_, _) => InvalidatePreview();

+ 5 - 5
QuestPDF.Previewer/QuestPDF.Previewer.csproj

@@ -11,11 +11,11 @@
   </ItemGroup>
   </ItemGroup>
 
 
 	<ItemGroup>
 	<ItemGroup>
-		<PackageReference Include="Avalonia" Version="0.10.13" />
-		<PackageReference Include="Avalonia.Desktop" Version="0.10.13" />
-		<PackageReference Include="Avalonia.Diagnostics" Version="0.10.13" />
-		<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.13" />
-		<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
+		<PackageReference Include="Avalonia" Version="0.10.10" />
+		<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
+		<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
+		<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
+		<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
 		<PackageReference Include="ReactiveUI" Version="17.1.50" />
 		<PackageReference Include="ReactiveUI" Version="17.1.50" />
 		<PackageReference Include="System.Reactive" Version="5.0.0" />
 		<PackageReference Include="System.Reactive" Version="5.0.0" />
 	</ItemGroup>
 	</ItemGroup>