Skip to content

Commit 2ffbce0

Browse files
committed
add custom AcrylicBorder control
1 parent 07798ba commit 2ffbce0

File tree

4 files changed

+262
-0
lines changed

4 files changed

+262
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Avalonia.Controls.Primitives;
2+
using Avalonia.Media;
3+
4+
namespace Arise.Client.Launcher.Controls;
5+
6+
public class AcrylicBorder : TemplatedControl
7+
{
8+
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
9+
AvaloniaProperty.Register<AcrylicBorder, BoxShadows>(nameof(BoxShadow));
10+
11+
public static readonly StyledProperty<ExperimentalAcrylicMaterial> MaterialProperty =
12+
AvaloniaProperty.Register<AcrylicBorder, ExperimentalAcrylicMaterial>(nameof(Material));
13+
14+
public BoxShadows BoxShadow
15+
{
16+
get => GetValue(BoxShadowProperty);
17+
set => SetValue(BoxShadowProperty, value);
18+
}
19+
20+
public ExperimentalAcrylicMaterial Material
21+
{
22+
get => GetValue(MaterialProperty);
23+
set => SetValue(MaterialProperty, value);
24+
}
25+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
using Avalonia.Media;
2+
using Avalonia.Rendering.SceneGraph;
3+
using Avalonia.Skia;
4+
using SkiaSharp;
5+
6+
namespace Arise.Client.Launcher.Controls;
7+
8+
// Taken from: https://gist.github.com/kekekeks/ac06098a74fe87d49a9ff9ea37fa67bc
9+
// - modified to support rounded corners with different radius per corner
10+
// - added partial designer support (from the first comment in the gist)
11+
// - exposed blur radius as a StyledProperty
12+
public sealed class CustomBlurBehind : Control
13+
{
14+
private static readonly ImmutableExperimentalAcrylicMaterial DefaultAcrylicMaterial =
15+
(ImmutableExperimentalAcrylicMaterial)new ExperimentalAcrylicMaterial()
16+
{
17+
MaterialOpacity = 0.1,
18+
TintColor = Colors.White,
19+
TintOpacity = 0,
20+
PlatformTransparencyCompensationLevel = 0,
21+
}.ToImmutable();
22+
23+
public static readonly StyledProperty<ExperimentalAcrylicMaterial> MaterialProperty =
24+
AvaloniaProperty.Register<CustomBlurBehind, ExperimentalAcrylicMaterial>(nameof(Material));
25+
26+
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
27+
AvaloniaProperty.Register<CustomBlurBehind, CornerRadius>(nameof(CornerRadius));
28+
29+
public static readonly StyledProperty<float> BlurRadiusProperty =
30+
AvaloniaProperty.Register<CustomBlurBehind, float>(nameof(BlurRadius), 7);
31+
32+
private static SKShader? _acrylicNoiseShader;
33+
34+
public ExperimentalAcrylicMaterial Material
35+
{
36+
get => GetValue(MaterialProperty);
37+
set => SetValue(MaterialProperty, value);
38+
}
39+
40+
public CornerRadius CornerRadius
41+
{
42+
get => GetValue(CornerRadiusProperty);
43+
set => SetValue(CornerRadiusProperty, value);
44+
}
45+
46+
public float BlurRadius
47+
{
48+
get => GetValue(BlurRadiusProperty);
49+
set => SetValue(BlurRadiusProperty, value);
50+
}
51+
52+
static CustomBlurBehind()
53+
{
54+
AffectsRender<CustomBlurBehind>(MaterialProperty);
55+
}
56+
57+
public override void Render(DrawingContext context)
58+
{
59+
var mat = Material != null
60+
? (ImmutableExperimentalAcrylicMaterial)Material.ToImmutable()
61+
: DefaultAcrylicMaterial;
62+
#pragma warning disable CA2000 // todo: handle this
63+
context.Custom(new BlurBehindRenderOperation(
64+
mat,
65+
new RoundedRect(
66+
new Rect(default, Bounds.Size),
67+
CornerRadius.TopLeft,
68+
CornerRadius.TopRight,
69+
CornerRadius.BottomRight,
70+
CornerRadius.BottomLeft),
71+
BlurRadius));
72+
#pragma warning restore CA2000
73+
}
74+
75+
private sealed class BlurBehindRenderOperation : ICustomDrawOperation
76+
{
77+
private readonly ImmutableExperimentalAcrylicMaterial _material;
78+
private readonly RoundedRect _bounds;
79+
private readonly float _blurRadius;
80+
81+
public Rect Bounds => _bounds.Rect.Inflate(4);
82+
83+
public BlurBehindRenderOperation(ImmutableExperimentalAcrylicMaterial material, RoundedRect bounds, float blurRadius)
84+
{
85+
_material = material;
86+
_bounds = bounds;
87+
_blurRadius = blurRadius;
88+
}
89+
90+
public bool HitTest(Point p)
91+
{
92+
return _bounds.ContainsExclusive(p);
93+
}
94+
95+
private static SKColorFilter CreateAlphaColorFilter(double opacity)
96+
{
97+
if (opacity > 1)
98+
opacity = 1;
99+
var c = new byte[256];
100+
var a = new byte[256];
101+
for (var i = 0; i < 256; i++)
102+
{
103+
c[i] = (byte)i;
104+
a[i] = (byte)(i * opacity);
105+
}
106+
107+
return SKColorFilter.CreateTable(a, c, c, c);
108+
}
109+
110+
public void Render(ImmediateDrawingContext context)
111+
{
112+
var feature = context.TryGetFeature<ISkiaSharpApiLeaseFeature>();
113+
if (feature == null)
114+
return;
115+
116+
using var skia = feature.Lease();
117+
118+
if (skia == null)
119+
return;
120+
121+
if (!skia.SkCanvas.TotalMatrix.TryInvert(out var currentInvertedTransform))
122+
return;
123+
124+
using var backgroundSnapshot = skia.SkSurface!.Snapshot(); // todo: handle null SkSurface
125+
using var backdropShader = SKShader.CreateImage(
126+
backgroundSnapshot,
127+
SKShaderTileMode.Clamp,
128+
SKShaderTileMode.Clamp,
129+
currentInvertedTransform);
130+
131+
using var skrrect = new SKRoundRect(new SKRect(0, 0, (float)_bounds.Rect.Width, (float)_bounds.Rect.Height));
132+
skrrect.SetRectRadii(skrrect.Rect, [new SKPoint((float)_bounds.RadiiTopLeft.X, (float)_bounds.RadiiTopLeft.Y), new SKPoint((float)_bounds.RadiiTopRight.X, (float)_bounds.RadiiTopRight.Y), new SKPoint((float)_bounds.RadiiBottomRight.X, (float)_bounds.RadiiBottomRight.Y), new SKPoint((float)_bounds.RadiiBottomLeft.X, (float)_bounds.RadiiBottomLeft.Y)]);
133+
134+
// todo: fix this (it's for designer only)
135+
if (skia.GrContext == null)
136+
{
137+
using var designerFilter = SKImageFilter.CreateBlur(_blurRadius, _blurRadius, SKShaderTileMode.Clamp);
138+
using var tmp = new SKPaint()
139+
{
140+
Shader = backdropShader,
141+
ImageFilter = designerFilter,
142+
};
143+
skia.SkCanvas.DrawRoundRect(skrrect, tmp);
144+
145+
return;
146+
}
147+
148+
using var blurred = SKSurface.Create(skia.GrContext, false, new SKImageInfo(
149+
(int)Math.Ceiling(_bounds.Rect.Width),
150+
(int)Math.Ceiling(_bounds.Rect.Height),
151+
SKImageInfo.PlatformColorType,
152+
SKAlphaType.Premul));
153+
using var filter = SKImageFilter.CreateBlur(_blurRadius, _blurRadius, SKShaderTileMode.Clamp);
154+
using var blurPaint = new SKPaint
155+
{
156+
Shader = backdropShader,
157+
ImageFilter = filter,
158+
};
159+
160+
blurred.Canvas.DrawRoundRect(skrrect, blurPaint);
161+
162+
using var blurSnap = blurred.Snapshot();
163+
using var blurSnapShader = SKShader.CreateImage(blurSnap);
164+
using var blurSnapPaint = new SKPaint
165+
{
166+
Shader = blurSnapShader,
167+
IsAntialias = true,
168+
};
169+
170+
skia.SkCanvas.DrawRoundRect(skrrect, blurSnapPaint);
171+
172+
using var acrylliPaint = new SKPaint();
173+
acrylliPaint.IsAntialias = true;
174+
175+
const double noiseOpacity = 0.0225;
176+
177+
var tintColor = _material.TintColor;
178+
var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A);
179+
180+
if (_acrylicNoiseShader == null)
181+
{
182+
using var stream = typeof(SkiaPlatform).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png");
183+
using var bitmap = SKBitmap.Decode(stream);
184+
#pragma warning disable CA2000 // Elimina gli oggetti prima che siano esterni all'ambito
185+
_acrylicNoiseShader = SKShader.CreateBitmap(
186+
bitmap,
187+
SKShaderTileMode.Repeat,
188+
SKShaderTileMode.Repeat)
189+
.WithColorFilter(CreateAlphaColorFilter(noiseOpacity));
190+
#pragma warning restore CA2000 // Elimina gli oggetti prima che siano esterni all'ambito
191+
}
192+
193+
using var backdrop = SKShader.CreateColor(new SKColor(_material.MaterialColor.R, _material.MaterialColor.G, _material.MaterialColor.B, _material.MaterialColor.A));
194+
using var tintShader = SKShader.CreateColor(tint);
195+
using var effectiveTint = SKShader.CreateCompose(backdrop, tintShader);
196+
using var compose = SKShader.CreateCompose(effectiveTint, _acrylicNoiseShader);
197+
acrylliPaint.Shader = compose;
198+
acrylliPaint.IsAntialias = true;
199+
skia.SkCanvas.DrawRoundRect(skrrect, acrylliPaint);
200+
}
201+
202+
public bool Equals(ICustomDrawOperation? other)
203+
{
204+
return other is BlurBehindRenderOperation op && op._bounds == _bounds && op._material.Equals(_material);
205+
}
206+
207+
public void Dispose()
208+
{
209+
}
210+
}
211+
}

src/client/Launcher/LauncherApplication.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<icons:MaterialIconStyles />
88
<StyleInclude Source="/Launcher/Styles/Window.axaml" />
99
<StyleInclude Source="/Launcher/Styles/CheckBox.axaml" />
10+
<StyleInclude Source="/Launcher/Styles/AcrylicBorder.axaml" />
1011
</Application.Styles>
1112
<Application.Resources>
1213
<ResourceDictionary>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Styles xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:controllers="using:Arise.Client.Launcher.Controllers"
4+
xmlns:controls="using:Arise.Client.Launcher.Controls"
5+
x:DataType="controllers:LauncherController">
6+
<!-- Adding this here with different sizes, as the size properties of
7+
the checkbox rectangle don't seem to work when accessed via
8+
the /template/ selector -->
9+
<Style Selector="controls|AcrylicBorder">
10+
<Setter Property="ClipToBounds" Value="False"/>
11+
<Setter Property="Template">
12+
<ControlTemplate>
13+
<Panel Width="{TemplateBinding Width}"
14+
Height="{TemplateBinding Height}">
15+
<Border BoxShadow="{TemplateBinding BoxShadow}"
16+
Background="Transparent"
17+
CornerRadius="{TemplateBinding CornerRadius}"/>
18+
<controls:CustomBlurBehind
19+
CornerRadius="{TemplateBinding CornerRadius}"
20+
Material="{TemplateBinding Material}"/>
21+
</Panel>
22+
</ControlTemplate>
23+
</Setter>
24+
</Style>
25+
</Styles>

0 commit comments

Comments
 (0)