From 321cba32ced4347d601a2ada1e2d1caf55fb7af4 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Mon, 21 Mar 2022 01:02:53 +0100 Subject: [PATCH] Implemented basic canvas interactions --- QuestPDF.Previewer.Examples/Program.cs | 10 +- .../QuestPDF.Previewer.Examples.csproj | 1 + QuestPDF.Previewer/FluentWindow.cs | 46 ----- QuestPDF.Previewer/InteractiveCanvas.cs | 168 ++++++++++++++++++ QuestPDF.Previewer/PreviewerApp.axaml | 2 +- QuestPDF.Previewer/PreviewerControl.cs | 98 ++++++++++ QuestPDF.Previewer/PreviewerPageControl.cs | 44 ----- QuestPDF.Previewer/PreviewerWindow.axaml | 69 +++---- QuestPDF.Previewer/PreviewerWindow.axaml.cs | 8 +- QuestPDF.Previewer/QuestPDF.Previewer.csproj | 10 +- 10 files changed, 311 insertions(+), 145 deletions(-) delete mode 100644 QuestPDF.Previewer/FluentWindow.cs create mode 100644 QuestPDF.Previewer/InteractiveCanvas.cs create mode 100644 QuestPDF.Previewer/PreviewerControl.cs delete mode 100644 QuestPDF.Previewer/PreviewerPageControl.cs 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 @@ - - - - - + - - - - - - - + - - - + + + - - -