From e03d6b20e9165d364223fe875097b429fac7668c Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 23 Jul 2023 17:19:56 +0200 Subject: [PATCH] Added acrylic blur test --- .../Controls/AcrylicBlur/AcrylicBlur.axaml | 22 +++ .../Controls/AcrylicBlur/AcrylicBlur.cs | 45 ++++++ .../AcrylicBlur/AcrylicBlurRenderOperation.cs | 138 ++++++++++++++++++ .../Workshop/Entries/EntryListView.axaml | 40 +++-- .../Workshop/Entries/EntryListView.axaml.cs | 5 - .../Workshop/Entries/EntryListViewModel.cs | 6 +- .../Workshop/Home/WorkshopHomeView.axaml | 115 +++++++++------ .../Workshop/Profile/ProfileListView.axaml | 17 +-- .../Workshop/Profile/ProfileListViewModel.cs | 56 +++++-- src/Artemis.UI/Styles/Artemis.axaml | 1 + 10 files changed, 353 insertions(+), 92 deletions(-) create mode 100644 src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.axaml create mode 100644 src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.cs create mode 100644 src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlurRenderOperation.cs diff --git a/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.axaml b/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.axaml new file mode 100644 index 000000000..ee706785b --- /dev/null +++ b/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.axaml @@ -0,0 +1,22 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.cs b/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.cs new file mode 100644 index 000000000..721ce1ebf --- /dev/null +++ b/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlur.cs @@ -0,0 +1,45 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace Artemis.UI.Controls.AcrylicBlur; + +public class AcrylicBlur : ContentControl +{ + private static readonly ImmutableExperimentalAcrylicMaterial DefaultAcrylicMaterial = (ImmutableExperimentalAcrylicMaterial) new ExperimentalAcrylicMaterial() + { + MaterialOpacity = 0.1, + TintColor = new Color(255, 7, 7, 7), + TintOpacity = 1, + PlatformTransparencyCompensationLevel = 0 + }.ToImmutable(); + + public static readonly StyledProperty MaterialProperty = + AvaloniaProperty.Register(nameof(Material)); + + public ExperimentalAcrylicMaterial? Material + { + get => GetValue(MaterialProperty); + set => SetValue(MaterialProperty, value); + } + + public static readonly StyledProperty BlurProperty = AvaloniaProperty.Register(nameof(Blur)); + + public int Blur + { + get => GetValue(BlurProperty); + set => SetValue(BlurProperty, value); + } + + static AcrylicBlur() + { + AffectsRender(MaterialProperty); + AffectsRender(BlurProperty); + } + + public override void Render(DrawingContext context) + { + ImmutableExperimentalAcrylicMaterial mat = Material != null ? (ImmutableExperimentalAcrylicMaterial) Material.ToImmutable() : DefaultAcrylicMaterial; + context.Custom(new AcrylicBlurRenderOperation(this, mat, Blur, new Rect(default, Bounds.Size))); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlurRenderOperation.cs b/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlurRenderOperation.cs new file mode 100644 index 000000000..326c9aefa --- /dev/null +++ b/src/Artemis.UI/Controls/AcrylicBlur/AcrylicBlurRenderOperation.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using SkiaSharp; + +namespace Artemis.UI.Controls.AcrylicBlur; + +public class AcrylicBlurRenderOperation : ICustomDrawOperation +{ + private static SKShader? _acrylicNoiseShader; + + private readonly AcrylicBlur _acrylicBlur; + private readonly ImmutableExperimentalAcrylicMaterial _material; + private readonly int _blur; + private readonly Rect _bounds; + private SKImage? _backgroundSnapshot; + private bool _disposed; + + public AcrylicBlurRenderOperation(AcrylicBlur acrylicBlur, ImmutableExperimentalAcrylicMaterial material, int blur, Rect bounds) + { + _acrylicBlur = acrylicBlur; + _material = material; + _blur = blur; + _bounds = bounds; + } + + public void Dispose() + { + if (_disposed) + return; + + _backgroundSnapshot?.Dispose(); + _disposed = true; + } + + public bool HitTest(Point p) => _bounds.Contains(p); + + static SKColorFilter CreateAlphaColorFilter(double opacity) + { + if (opacity > 1) + opacity = 1; + byte[] c = new byte[256]; + byte[] a = new byte[256]; + for (int i = 0; i < 256; i++) + { + c[i] = (byte) i; + a[i] = (byte) (i * opacity); + } + + return SKColorFilter.CreateTable(a, c, c, c); + } + + public void Render(ImmediateDrawingContext context) + { + if (_disposed) + throw new ObjectDisposedException(nameof(AcrylicBlurRenderOperation)); + + ISkiaSharpApiLeaseFeature? leaseFeature = context.PlatformImpl.GetFeature(); + if (leaseFeature == null) + return; + using ISkiaSharpApiLease lease = leaseFeature.Lease(); + + if (!lease.SkCanvas.TotalMatrix.TryInvert(out SKMatrix currentInvertedTransform) || lease.SkSurface == null) + return; + + if (lease.SkCanvas.GetLocalClipBounds(out SKRect bounds) && !bounds.Contains(SKRect.Create(bounds.Left, bounds.Top, (float) _acrylicBlur.Bounds.Width, (float) _acrylicBlur.Bounds.Height))) + { + Dispatcher.UIThread.Invoke(() => _acrylicBlur.InvalidateVisual()); + } + else + { + _backgroundSnapshot?.Dispose(); + _backgroundSnapshot = lease.SkSurface.Snapshot(); + } + + _backgroundSnapshot ??= lease.SkSurface.Snapshot(); + using SKShader? backdropShader = SKShader.CreateImage(_backgroundSnapshot, SKShaderTileMode.Clamp, SKShaderTileMode.Clamp, currentInvertedTransform); + using SKSurface? blurred = SKSurface.Create( + lease.GrContext, + false, + new SKImageInfo((int) Math.Ceiling(_bounds.Width), (int) Math.Ceiling(_bounds.Height), SKImageInfo.PlatformColorType, SKAlphaType.Premul) + ); + using (SKImageFilter? filter = SKImageFilter.CreateBlur(_blur, _blur, SKShaderTileMode.Clamp)) + using (SKPaint blurPaint = new SKPaint {Shader = backdropShader, ImageFilter = filter}) + { + blurred.Canvas.DrawRect(0, 0, (float) _bounds.Width, (float) _bounds.Height, blurPaint); + + using (SKImage? blurSnap = blurred.Snapshot()) + using (SKShader? blurSnapShader = SKShader.CreateImage(blurSnap)) + using (SKPaint blurSnapPaint = new SKPaint {Shader = blurSnapShader, IsAntialias = true}) + { + // Rendering twice to reduce opacity + lease.SkCanvas.DrawRect(0, 0, (float) _bounds.Width, (float) _bounds.Height, blurSnapPaint); + lease.SkCanvas.DrawRect(0, 0, (float) _bounds.Width, (float) _bounds.Height, blurSnapPaint); + } + + //return; + using SKPaint acrylliPaint = new SKPaint(); + acrylliPaint.IsAntialias = true; + + double opacity = 1; + + const double noiseOpacity = 0.0225; + + Color tintColor = _material.TintColor; + SKColor tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); + + if (_acrylicNoiseShader == null) + { + using Stream? stream = typeof(SkiaPlatform).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png"); + using SKBitmap? bitmap = SKBitmap.Decode(stream); + _acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat).WithColorFilter(CreateAlphaColorFilter(noiseOpacity)); + } + + using (SKShader? backdrop = SKShader.CreateColor(new SKColor(_material.MaterialColor.R, _material.MaterialColor.G, _material.MaterialColor.B, _material.MaterialColor.A))) + using (SKShader? tintShader = SKShader.CreateColor(tint)) + using (SKShader? effectiveTint = SKShader.CreateCompose(backdrop, tintShader)) + using (SKShader? compose = SKShader.CreateCompose(effectiveTint, _acrylicNoiseShader)) + { + acrylliPaint.Shader = compose; + acrylliPaint.IsAntialias = true; + lease.SkCanvas.DrawRect(0, 0, (float) _bounds.Width, (float) _bounds.Height, acrylliPaint); + } + } + } + + public Rect Bounds => _bounds.Inflate(4); + + public bool Equals(ICustomDrawOperation? other) + { + return other is AcrylicBlurRenderOperation op && op._bounds == _bounds && op._material.Equals(_material); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml index e7450f685..9af92602f 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml @@ -5,31 +5,40 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:entries="clr-namespace:Artemis.UI.Screens.Workshop.Profile" xmlns:entries1="clr-namespace:Artemis.UI.Screens.Workshop.Entries" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="120" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="110" x:Class="Artemis.UI.Screens.Workshop.Entries.EntryListView" x:DataType="entries1:EntryListViewModel"> - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml.cs index 42a7b86ad..4ce9dc9dd 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryListView.axaml.cs @@ -16,9 +16,4 @@ public partial class EntryListView : ReactiveUserControl { AvaloniaXamlLoader.Load(this); } - - private async void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - await ViewModel.NavigateToEntry(); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs index dc393b80c..0691c5b53 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs @@ -1,8 +1,10 @@ using System; +using System.Reactive; using System.Threading.Tasks; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Entries; @@ -14,11 +16,13 @@ public class EntryListViewModel : ViewModelBase { _router = router; Entry = entry; + NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry); } public IGetEntries_Entries_Items Entry { get; } + public ReactiveCommand NavigateToEntry { get; } - public async Task NavigateToEntry() + private async Task ExecuteNavigateToEntry() { switch (Entry.EntryType) { diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml index a22fc4448..b94899712 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml @@ -4,55 +4,78 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:home="clr-namespace:Artemis.UI.Screens.Workshop.Home" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:controls="clr-namespace:Artemis.UI.Controls" + xmlns:acrylicBlur="clr-namespace:Artemis.UI.Controls.AcrylicBlur" + mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Workshop.Home.WorkshopHomeView" x:DataType="home:WorkshopHomeViewModel"> - - + - - - - - - - - - + Source="/Assets/Images/workshop-banner.jpg" + Height="200" + Stretch="UniformToFill" + RenderOptions.BitmapInterpolationMode="HighQuality"> + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test + + + - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml index 586bcdd75..3ae086124 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml @@ -16,20 +16,10 @@ - - - Filters - - - - - - - - - - + + + @@ -41,6 +31,7 @@ , IWorkshopViewModel { private readonly IRouter _router; + private readonly INotificationService _notificationService; private readonly IWorkshopClient _workshopClient; private readonly ObservableAsPropertyHelper _showPagination; + private readonly ObservableAsPropertyHelper _isLoading; private List? _entries; private int _page; + private int _loadedPage = -1; private int _totalPages = 1; - private int _entriesPerPage = 5; + private int _entriesPerPage = 10; - public ProfileListViewModel(IWorkshopClient workshopClient, IRouter router, CategoriesViewModel categoriesViewModel) + public ProfileListViewModel(IWorkshopClient workshopClient, IRouter router, CategoriesViewModel categoriesViewModel, INotificationService notificationService) { _workshopClient = workshopClient; _router = router; + _notificationService = notificationService; _showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination); - + _isLoading = this.WhenAnyValue(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading); + CategoriesViewModel = categoriesViewModel; // Respond to page changes @@ -49,6 +56,8 @@ public class ProfileListViewModel : RoutableScreen _showPagination.Value; + public bool IsLoading => _isLoading.Value; + public CategoriesViewModel CategoriesViewModel { get; } public List? Entries @@ -63,6 +72,12 @@ public class ProfileListViewModel : RoutableScreen RaiseAndSetIfChanged(ref _page, value); } + public int LoadedPage + { + get => _loadedPage; + set => RaiseAndSetIfChanged(ref _loadedPage, value); + } + public int TotalPages { get => _totalPages; @@ -78,25 +93,42 @@ public class ProfileListViewModel : RoutableScreen entries = await _workshopClient.GetEntries.ExecuteAsync(filter, EntriesPerPage * (Page - 1), EntriesPerPage, cancellationToken); - if (!entries.IsErrorResult() && entries.Data?.Entries?.Items != null) + try { - Entries = entries.Data.Entries.Items.Select(n => new EntryListViewModel(n, _router)).ToList(); - TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) EntriesPerPage); + EntryFilterInput filter = GetFilter(); + IOperationResult entries = await _workshopClient.GetEntries.ExecuteAsync(filter, EntriesPerPage * (Page - 1), EntriesPerPage, cancellationToken); + entries.EnsureNoErrors(); + + if (entries.Data?.Entries?.Items != null) + { + Entries = entries.Data.Entries.Items.Select(n => new EntryListViewModel(n, _router)).ToList(); + TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) EntriesPerPage); + } + else + TotalPages = 1; + } + catch (Exception e) + { + _notificationService.CreateNotification() + .WithTitle("Failed to load entries") + .WithMessage(e.Message) + .WithSeverity(NotificationSeverity.Error) + .Show(); + } + finally + { + LoadedPage = Page; } - else - TotalPages = 1; } private EntryFilterInput GetFilter() diff --git a/src/Artemis.UI/Styles/Artemis.axaml b/src/Artemis.UI/Styles/Artemis.axaml index a121ec71b..8e62c47f2 100644 --- a/src/Artemis.UI/Styles/Artemis.axaml +++ b/src/Artemis.UI/Styles/Artemis.axaml @@ -13,6 +13,7 @@ +