Implemented basic canvas interactions
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QuestPDF.Previewer\QuestPDF.Previewer.csproj" />
|
||||
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
|
||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Dark"/>
|
||||
<FluentTheme Mode="Dark" />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -6,55 +6,38 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="QuestPDF.Previewer.PreviewerWindow"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ExtendClientAreaToDecorationsHint="true"
|
||||
ExtendClientAreaTitleBarHeightHint="-1"
|
||||
Background="#666"
|
||||
Icon="/Images/Logo.png"
|
||||
UseLayoutRounding="True"
|
||||
Background="{x:Null}"
|
||||
x:Name="Window"
|
||||
Title="QuestPDF Document Preview">
|
||||
<previewer:FluentWindow.Styles>
|
||||
<Style Selector="TitleBar:fullscreen">
|
||||
<Setter Property="Background" Value="#F00" />
|
||||
</Style>
|
||||
</previewer:FluentWindow.Styles>
|
||||
|
||||
|
||||
<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>
|
||||
<RowDefinition Height="40"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</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>
|
||||
</Panel>
|
||||
</previewer:FluentWindow>
|
||||
</Window>
|
||||
@@ -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<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)));
|
||||
HotReloadManager.UpdateApplicationRequested += (_, _) => InvalidatePreview();
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
</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="System.Reactive" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user