-
Notifications
You must be signed in to change notification settings - Fork 0
/
SKXamlCanvas.axaml.cs
192 lines (167 loc) · 6.5 KB
/
SKXamlCanvas.axaml.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Skia;
using Avalonia.Threading;
using SkiaSharp;
namespace HierarchyGrid.Avalonia
{
public partial class SKXamlCanvas : Canvas
{
/// <summary>
/// Event to externally paint the Skia surface (using the <see cref="SKCanvas"/>).
/// </summary>
public event EventHandler<SKPaintSurfaceEventArgs>? PaintSurface;
private static readonly Vector Dpi = new Vector(96, 96);
private bool _ignorePixelScaling;
/// <summary>
/// Initializes a new instance of the <see cref="SKXamlCanvas"/> class.
/// </summary>
public SKXamlCanvas() { }
/// <summary>
/// Gets the current pixel size of the canvas.
/// Any scaling factor is already applied.
/// </summary>
public Size CanvasSize { get; private set; }
/// <summary>
/// Gets the current render scaling applied to the control.
/// </summary>
public double Scale { get; private set; } = 1;
/// <summary>
/// Gets or sets a value indicating whether the canvas's resolution and scale
/// will be automatically adjusted to match physical device pixels.
/// </summary>
public bool IgnorePixelScaling
{
get => this._ignorePixelScaling;
set
{
this._ignorePixelScaling = value;
this.Invalidate();
}
}
/*
private void OnDpiChanged(DisplayInformation sender, object args = null)
{
Dpi = sender.LogicalDpi / DpiBase;
Invalidate();
}
*/
/// <summary>
/// Invalidates the canvas causing the surface to be repainted.
/// This will fire the <see cref="PaintSurface"/> event.
/// </summary>
public void Invalidate()
{
Dispatcher.UIThread.Post(() => this.RepaintSurface());
}
/// <summary>
/// Repaints the Skia surface and canvas.
/// </summary>
private void RepaintSurface()
{
if (!this.IsVisible)
{
return;
}
// Display scaling is important to consider here:
// The bitmap itself should be sized to match physical device pixels.
// This ensures it is never pixelated and renders properly to the display.
// However, in several cases physical pixels do not match the logical pixels.
// We also don't want to have to consider scaling in external code when calculating graphics.
// To make this easiest, the layout scaling factor is calculated and then used
// to find the size of the bitmap. This ensures it will match device pixels.
// Then the canvas undoes this by setting a scale factor itself.
// This means external code can use logical pixel size and the canvas will transform as needed.
// Then the underlying bitmap is still at physical device pixel resolution.
if (this.IgnorePixelScaling)
{
this.Scale = 1;
}
else
{
this.Scale = LayoutHelper.GetLayoutScale(this);
}
int pixelWidth = Convert.ToInt32(this.Bounds.Width * this.Scale);
int pixelHeight = Convert.ToInt32(this.Bounds.Height * this.Scale);
this.CanvasSize = new Size(pixelWidth, pixelHeight);
// WriteableBitmap does not support zero-size dimensions
// Therefore, to avoid a crash, exit here if size is zero
if (pixelWidth == 0 || pixelHeight == 0)
{
this.Background = null;
return;
}
var bitmap = new WriteableBitmap(
new PixelSize(pixelWidth, pixelHeight),
Dpi,
PixelFormat.Bgra8888,
AlphaFormat.Premul
);
using (var framebuffer = bitmap.Lock())
{
var info = new SKImageInfo(
framebuffer.Size.Width,
framebuffer.Size.Height,
framebuffer.Format.ToSkColorType(),
SKAlphaType.Premul
);
var properties = new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal);
// It is not too expensive to re-create the SKSurface on each re-paint.
// See: https://groups.google.com/g/skia-discuss/c/3c10MvyaSug/m/UOr238asCgAJ
//
// When creating the SKSurface it is important to specify a pixel geometry
// A defined pixel geometry is required for some anti-aliasing algorithms such as ClearType
// Also see: https://github.com/AvaloniaUI/Avalonia/pull/9558
using (
var surface = SKSurface.Create(
info,
framebuffer.Address,
framebuffer.RowBytes,
properties
)
)
{
if (!this.IgnorePixelScaling)
{
surface.Canvas.Scale(Convert.ToSingle(this.Scale));
}
this.OnPaintSurface(new SKPaintSurfaceEventArgs(surface, info, info));
}
properties.Dispose();
}
this.Background = new ImageBrush(bitmap)
{
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Top,
Stretch = Stretch.Fill
};
}
/// <inheritdoc/>
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
this.Invalidate();
}
/// <summary>
/// Called when the canvas should repaint its surface.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
this.PaintSurface?.Invoke(this, e);
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsVisibleProperty)
{
this.Invalidate();
}
}
}
}