diff --git a/BackgroundTest/App.xaml b/BackgroundTest/App.xaml
new file mode 100644
index 000000000..49b3b0664
--- /dev/null
+++ b/BackgroundTest/App.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BackgroundTest/App.xaml.cs b/BackgroundTest/App.xaml.cs
new file mode 100644
index 000000000..2f23bab59
--- /dev/null
+++ b/BackgroundTest/App.xaml.cs
@@ -0,0 +1,23 @@
+using Microsoft.UI.Xaml;
+
+// ReSharper disable SwitchStatementMissingSomeEnumCasesNoDefault
+// ReSharper disable CommentTypo
+// ReSharper disable StringLiteralTypo
+
+namespace BackgroundTest;
+
+public partial class App
+{
+ private TestWindow? _window;
+
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnLaunched(LaunchActivatedEventArgs args)
+ {
+ _window = new TestWindow();
+ _window.Activate();
+ }
+}
diff --git a/BackgroundTest/BackgroundTest.csproj b/BackgroundTest/BackgroundTest.csproj
new file mode 100644
index 000000000..f68974757
--- /dev/null
+++ b/BackgroundTest/BackgroundTest.csproj
@@ -0,0 +1,104 @@
+
+
+
+ WinExe
+ BackgroundTest.MainEntryPoint
+ Debug;Release
+
+ BackgroundTest
+ Collapse
+ Collapse
+ Collapse
+ Collapse
+ Collapse Launcher Team
+ $(Company). neon-nyan, Cry0, bagusnl, shatyuka, gablm.
+ Copyright 2022-2025 $(Company)
+
+ 1.83.12
+ preview
+ enable
+
+ x64
+ net10.0-windows10.0.26100.0
+ 10.0.17763.0
+ win-x64
+ true
+
+ portable
+ false
+
+ true
+ true
+
+ true
+ true
+ false
+ false
+
+ true
+ true
+ false
+ true
+ false
+ true
+ true
+ false
+ false
+ true
+
+
+
+ Exe
+
+
+ DISABLE_XAML_GENERATED_MAIN;ENABLEUSERFEEDBACK;USEVELOPACK;USENEWZIPDECOMPRESS;ENABLEHTTPREPAIR;PREVIEW;DUMPGIJSON;SIMULATEGIHDR;GSPBYPASSGAMERUNNING;MHYPLUGINSUPPORT
+ full
+
+
+
+
+ DISABLE_XAML_GENERATED_MAIN;ENABLEUSERFEEDBACK;USEVELOPACK;USENEWZIPDECOMPRESS;ENABLEHTTPREPAIR;PREVIEW;MHYPLUGINSUPPORT
+ True
+ true
+
+
+
+
+ DISABLE_XAML_GENERATED_MAIN;ENABLEUSERFEEDBACK;USEVELOPACK;USENEWZIPDECOMPRESS;ENABLEHTTPREPAIR;MHYPLUGINSUPPORT
+ true
+ true
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BackgroundTest/CustomControl/BGTestImage1.jpg b/BackgroundTest/CustomControl/BGTestImage1.jpg
new file mode 100644
index 000000000..72052793d
Binary files /dev/null and b/BackgroundTest/CustomControl/BGTestImage1.jpg differ
diff --git a/BackgroundTest/CustomControl/BGTestImage2.jpg b/BackgroundTest/CustomControl/BGTestImage2.jpg
new file mode 100644
index 000000000..c261b608f
Binary files /dev/null and b/BackgroundTest/CustomControl/BGTestImage2.jpg differ
diff --git a/BackgroundTest/CustomControl/BGTestImage3.jpg b/BackgroundTest/CustomControl/BGTestImage3.jpg
new file mode 100644
index 000000000..4d3c179f0
Binary files /dev/null and b/BackgroundTest/CustomControl/BGTestImage3.jpg differ
diff --git a/BackgroundTest/CustomControl/BGTestImage4.jpg b/BackgroundTest/CustomControl/BGTestImage4.jpg
new file mode 100644
index 000000000..0f04eeeb4
Binary files /dev/null and b/BackgroundTest/CustomControl/BGTestImage4.jpg differ
diff --git a/BackgroundTest/CustomControl/ChangedStructItemArgs.cs b/BackgroundTest/CustomControl/ChangedStructItemArgs.cs
new file mode 100644
index 000000000..714c9f579
--- /dev/null
+++ b/BackgroundTest/CustomControl/ChangedStructItemArgs.cs
@@ -0,0 +1,15 @@
+namespace BackgroundTest.CustomControl;
+
+public readonly struct ChangedStructItemArgs(TItem oldItem, TItem newItem)
+ where TItem : struct
+{
+ public TItem OldItem { get; private init; } = oldItem;
+ public TItem NewItem { get; private init; } = newItem;
+}
+
+public class ChangedObjectItemArgs(TItem? oldItem, TItem? newItem)
+ where TItem : class
+{
+ public TItem? OldItem { get; private init; } = oldItem;
+ public TItem? NewItem { get; private init; } = newItem;
+}
\ No newline at end of file
diff --git a/BackgroundTest/CustomControl/Extensions.cs b/BackgroundTest/CustomControl/Extensions.cs
new file mode 100644
index 000000000..7c8055bc5
--- /dev/null
+++ b/BackgroundTest/CustomControl/Extensions.cs
@@ -0,0 +1,110 @@
+using Microsoft.UI.Input;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace BackgroundTest.CustomControl;
+
+public static class Extensions
+{
+ public static readonly DependencyProperty CursorTypeProperty =
+ DependencyProperty.RegisterAttached("CursorType", typeof(InputSystemCursorShape),
+ typeof(UIElement), new PropertyMetadata(InputSystemCursorShape.Arrow));
+
+ public static InputSystemCursorShape GetCursorType(DependencyObject obj) => (InputSystemCursorShape)obj.GetValue(CursorTypeProperty);
+
+ public static void SetCursorType(DependencyObject obj, InputSystemCursorShape value)
+ {
+ InputSystemCursor? cursor = InputSystemCursor.Create(value);
+ if (cursor is null ||
+ obj is not UIElement asElement)
+ {
+ return;
+ }
+
+ asElement.SetCursor(cursor);
+ asElement.SetValue(CursorTypeProperty, value);
+ }
+
+ extension(Control source)
+ {
+ [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetTemplateChild")]
+ private extern DependencyObject GetTemplateChildAccessor(string name);
+
+ internal T GetTemplateChild(string name)
+ where T : class
+ {
+ DependencyObject obj = source.GetTemplateChildAccessor(name);
+ if (obj is not T castObj)
+ {
+ throw new
+ InvalidCastException($"Cannot cast type to: {typeof(T).Name} as the object expects type: {obj.GetType().Name}");
+ }
+
+ return castObj;
+ }
+ }
+
+ /// The member of an element
+ extension(UIElement element)
+ {
+ ///
+ /// Set the cursor for the element.
+ ///
+ /// The cursor you want to set. Use to choose the cursor you want to set.
+ [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_ProtectedCursor")]
+ private extern void SetCursor(InputCursor inputCursor);
+ }
+
+ internal static double TryGetDouble(this object? obj)
+ {
+ return obj switch
+ {
+ sbyte asSbyte => asSbyte,
+ byte asByte => asByte,
+ ushort asUshort => asUshort,
+ short asShort => asShort,
+ uint asUint => asUint,
+ int asInt => asInt,
+ ulong asUlong => asUlong,
+ long asLong => asLong,
+ float asFloat => asFloat,
+ double asDouble => asDouble,
+ _ => double.NaN
+ };
+ }
+
+ internal static void BindProperty(this FrameworkElement element,
+ FrameworkElement source,
+ string propertyName,
+ DependencyProperty dependencyProperty,
+ BindingMode bindingMode)
+ {
+ element.SetBinding(dependencyProperty, new Binding
+ {
+ Mode = bindingMode,
+ Source = source,
+ Path = new PropertyPath(propertyName)
+ });
+ }
+
+ internal static Uri GetStringAsUri(this string asStringSource)
+ {
+ // Try to create URL with absolute path.
+ // If not (assume it's a relative local path), then try to get the fully qualified local path.
+ if (Uri.TryCreate(asStringSource, UriKind.Absolute, out Uri? sourceUri) ||
+ Path.IsPathFullyQualified(asStringSource))
+ {
+ return sourceUri ?? new Uri(asStringSource);
+ }
+
+ string currentWorkingDir = Directory.GetCurrentDirectory();
+ ReadOnlySpan asStringSourceSpan = asStringSource.Trim("/\\");
+ asStringSource = Path.Join(currentWorkingDir, asStringSourceSpan);
+
+ return sourceUri ?? new Uri(asStringSource);
+ }
+}
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/IMediaCacheHandler.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/IMediaCacheHandler.cs
new file mode 100644
index 000000000..29ba317bf
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/IMediaCacheHandler.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+
+///
+/// An interface which implements media cache handler for loading cached media sources.
+///
+public interface IMediaCacheHandler
+{
+ ///
+ /// Loads the media source from cached local path or URL or Seekable and Readable .
+ ///
+ /// The input source to be cached.
+ /// An instance of which defines the cached source and its properties.
+ Task LoadCachedSource(object? sourceObject);
+}
+
+///
+/// Contains the result and properties of the cached media source.
+///
+public class MediaCacheResult
+{
+ ///
+ /// Indicates whether to force using the WIC (Windows Imaging Component) decoder for image source or not.
+ ///
+ public bool ForceUseInternalDecoder { get; set; }
+
+ ///
+ /// The source of the media. Only or for local path and URL, or Seekable and Readable types are supported.
+ ///
+ public object? CachedSource { get; set; }
+
+ ///
+ /// Whether to perform Dispose if is a type.
+ ///
+ public bool DisposeStream { get; set; }
+}
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.FrameRenderer.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.FrameRenderer.cs
new file mode 100644
index 000000000..a1af4b223
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.FrameRenderer.cs
@@ -0,0 +1,283 @@
+using Hi3Helper.Win32.ManagedTools;
+using Hi3Helper.Win32.Native.Interfaces.DXGI;
+using Hi3Helper.Win32.Native.Structs;
+using Hi3Helper.Win32.WinRT.SwapChainPanelHelper;
+using Microsoft.Graphics.Canvas;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media.Imaging;
+using System;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Windows.Media.Playback;
+using WinRT;
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+
+public partial class LayeredBackgroundImage
+{
+ #region Properties
+
+ // ReSharper disable once InconsistentNaming
+ private static ref readonly Guid IMediaPlayer_IID
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ ReadOnlySpan span = [
+ 253, 55, 229, 207, 106, 248, 70, 68, 191, 77,
+ 200, 231, 146, 183, 180, 179
+ ];
+ return ref Unsafe.As(ref MemoryMarshal.GetReference(span));
+ }
+ }
+
+ #endregion
+
+ #region Fields
+
+ private volatile int _isBlockVideoFrameDraw = 1;
+ private int _isVideoFrameDrawInProgress;
+ private TimeSpan _lastVideoPlayerPosition;
+
+ #endregion
+
+ #region Video Frame Drawing
+
+ private void VideoPlayer_VideoFrameAvailable(MediaPlayer sender, object args)
+ {
+ if (_canvasSurfaceImageSourceNative == null)
+ {
+ return;
+ }
+
+ if (_isBlockVideoFrameDraw == 1 ||
+ Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1)
+ {
+ return;
+ }
+
+ DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, DrawFrame);
+ return;
+
+ void DrawFrame()
+ {
+ try
+ {
+ SwapChainPanelHelper.NativeSurfaceImageSource_BeginDrawUnsafe(_canvasSurfaceImageSourceNativePtr, in _canvasRenderArea, out nint surfacePpv);
+ SwapChainPanelHelper.MediaPlayer_CopyFrameToVideoSurfaceUnsafe(_videoPlayerPtr, surfacePpv);
+ SwapChainPanelHelper.NativeSurfaceImageSource_EndDrawUnsafe(_canvasSurfaceImageSourceNativePtr);
+ Marshal.Release(surfacePpv);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+ finally
+ {
+ Volatile.Write(ref _isVideoFrameDrawInProgress, 0);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Video Frame Initialization / Disposal
+
+ private void InitializeVideoPlayer()
+ {
+ if (_videoPlayer != null!)
+ {
+ return;
+ }
+
+ _videoPlayer = new MediaPlayer
+ {
+ AutoPlay = false,
+ IsLoopingEnabled = true,
+ IsVideoFrameServerEnabled = true,
+ Volume = AudioVolume.GetClampedVolume(),
+ IsMuted = !IsAudioEnabled
+ };
+
+ ComMarshal.TryGetComInterfaceReference(_videoPlayer,
+ in IMediaPlayer_IID,
+ out _videoPlayerPtr,
+ out _,
+ CreateComInterfaceFlags.None,
+ true);
+ _videoPlayer.MediaOpened += InitializeVideoFrameOnMediaOpened;
+ }
+
+ private void DisposeVideoPlayer()
+ {
+ if (_videoPlayer == null!)
+ {
+ return;
+ }
+
+ // Save last video player duration for later
+ if (_videoPlayer.CanSeek)
+ {
+ _lastVideoPlayerPosition = _videoPlayer.Position;
+ }
+
+ _videoPlayer.Dispose();
+ _videoPlayer.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailable;
+ _videoPlayer.MediaOpened -= InitializeVideoFrameOnMediaOpened;
+ Interlocked.Exchange(ref _videoPlayer!, null);
+ _videoPlayerPtr = nint.Zero;
+ }
+
+ private void InitializeRenderTargetSize(MediaPlaybackSession playbackSession)
+ {
+ double currentCanvasWidth = playbackSession.NaturalVideoWidth;
+ double currentCanvasHeight = playbackSession.NaturalVideoHeight;
+
+ _canvasWidth = (int)(currentCanvasWidth * XamlRoot.RasterizationScale * 2d);
+ _canvasHeight = (int)(currentCanvasHeight * XamlRoot.RasterizationScale * 2d);
+ _canvasRenderArea = new Rect(0, 0, _canvasWidth, _canvasHeight);
+ }
+
+ private void InitializeRenderTarget()
+ {
+ DisposeRenderTarget(); // Always ensure the previous render target has been disposed
+
+ _canvasDevice = CanvasDevice.GetSharedDevice();
+ _canvasSurfaceImageSource = new SurfaceImageSource(_canvasWidth, _canvasHeight, true);
+ SwapChainPanelHelper.GetNativeSurfaceImageSource(_canvasSurfaceImageSource,
+ out _canvasSurfaceImageSourceNative,
+ out _canvasD3DDeviceContext);
+
+ ComMarshal.TryGetComInterfaceReference(_canvasSurfaceImageSourceNative!,
+ out _canvasSurfaceImageSourceNativePtr,
+ out _,
+ CreateComInterfaceFlags.None,
+ true);
+
+ if (FindRenderImage() is { } image)
+ {
+ image.Source = _canvasSurfaceImageSource;
+ }
+ }
+
+ private void DisposeRenderTarget()
+ {
+ // Dispose D3DDeviceContext and its dependencies
+ _canvasD3DDeviceContext?.Dispose();
+ _canvasSurfaceImageSourceNative?.SetDevice(nint.Zero);
+ ComMarshal.TryReleaseComObject(_canvasSurfaceImageSourceNative, out _);
+
+ _canvasDevice?.Dispose();
+ if (FindRenderImage() is { } image)
+ {
+ image.Source = null;
+ }
+
+ _canvasSurfaceImageSourceNativePtr = nint.Zero;
+
+ Interlocked.Exchange(ref _canvasDevice, null);
+ Interlocked.Exchange(ref _canvasD3DDeviceContext, null);
+ Interlocked.Exchange(ref _canvasSurfaceImageSourceNative, null);
+ Interlocked.Exchange(ref _canvasSurfaceImageSource, null);
+ }
+
+ private Image? FindRenderImage() => _backgroundGrid.Children
+ .OfType()
+ .LastOrDefault(x => x.Name == "VideoRenderFrame");
+
+ #endregion
+
+ #region Video Player Events
+
+ private static void IsAudioEnabled_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ LayeredBackgroundImage instance = (LayeredBackgroundImage)d;
+ if (instance._videoPlayer is not { } videoPlayer)
+ {
+ return;
+ }
+
+ videoPlayer.IsMuted = !(bool)e.NewValue;
+ }
+
+ private static void AudioVolume_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ LayeredBackgroundImage instance = (LayeredBackgroundImage)d;
+ if (instance._videoPlayer is not { } videoPlayer)
+ {
+ return;
+ }
+
+ double volume = e.NewValue.TryGetDouble();
+ videoPlayer.Volume = volume.GetClampedVolume();
+ }
+
+ public void Play()
+ {
+ try
+ {
+ if (_videoPlayer != null!)
+ {
+ InitializeRenderTarget();
+ _videoPlayer.VideoFrameAvailable += VideoPlayer_VideoFrameAvailable;
+ _videoPlayer.Play();
+ _isBlockVideoFrameDraw = 0;
+ }
+ else if (_lastBackgroundSourceType == MediaSourceType.Video &&
+ BackgroundSource != null)
+ {
+ // Try loading last media
+ LoadFromSourceAsyncDetached(BackgroundSourceProperty,
+ _lastBackgroundSource,
+ nameof(BackgroundStretch),
+ nameof(BackgroundHorizontalAlignment),
+ nameof(BackgroundVerticalAlignment),
+ _backgroundGrid,
+ true,
+ ref _lastBackgroundSourceType);
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+ finally
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+
+ public void Pause()
+ {
+ try
+ {
+ if (_videoPlayer != null!)
+ {
+ _isBlockVideoFrameDraw = 1;
+ _videoPlayer.Pause();
+ _videoPlayer.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailable;
+ DisposeRenderTarget();
+ DisposeVideoPlayer();
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+ finally
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+
+ #endregion
+}
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.Loaders.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.Loaders.cs
new file mode 100644
index 000000000..a4c5a4d26
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.Loaders.cs
@@ -0,0 +1,499 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Media.Animation;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.Media.Playback;
+using Windows.Storage.Streams;
+
+// ReSharper disable StringLiteralTypo
+// ReSharper disable CommentTypo
+#pragma warning disable CsWinRT1032
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+
+public partial class LayeredBackgroundImage
+{
+ #region Enums
+
+ private enum MediaSourceType
+ {
+ Unknown,
+ Image,
+ Video,
+ }
+
+ ///
+ /// All the sources in this list are common image formats supported by Windows Imaging Component (WIC).
+ /// Some formats might require additional codecs to be installed.
+ ///
+ /// The extensions list are taken from:
+ /// https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types#jpeg_joint_photographic_experts_group_image
+ ///
+ private static readonly HashSet SupportedImageBitmapExtensions = new([
+ ".jpg", ".jpeg", ".jpe", ".jif", ".jfif", // "image/jpeg"
+ ".apng", ".png", // "image/apng" and "image/png"
+ ".bmp", // "image/bmp"
+ ".gif", // "image/gif"
+ ".ico", // "image/x-icon"
+ ".tif", ".tiff", // "image/tiff"
+ ".xbm" // "image/xbm"
+ ], StringComparer.OrdinalIgnoreCase);
+ internal static readonly HashSet.AlternateLookup> SupportedImageBitmapExtensionsLookup =
+ SupportedImageBitmapExtensions.GetAlternateLookup>();
+
+ private static readonly HashSet SupportedImageBitmapExternalCodecExtensions = new([
+ ".jxr", // "image/jxr" (Requires additional codec)
+ ".avif", // "image/avif" (Requires additional codec)
+ ".webp" // "image/webp" (Requires additional codec)
+ ], StringComparer.OrdinalIgnoreCase);
+ internal static readonly HashSet.AlternateLookup> SupportedImageBitmapExternalCodecExtensionsLookup =
+ SupportedImageBitmapExternalCodecExtensions.GetAlternateLookup>();
+
+ private static readonly HashSet SupportedImageVectorExtensions = new([
+ ".svg" // "image/svg"
+ ], StringComparer.OrdinalIgnoreCase);
+ internal static readonly HashSet.AlternateLookup> SupportedImageVectorExtensionsLookup =
+ SupportedImageVectorExtensions.GetAlternateLookup>();
+
+ private static readonly HashSet SupportedVideoExtensions = new([
+ ".3gp", ".3gp2", // "video/3gp"
+ ".asf", ".wmv", // "video/wmv"
+ ".avi", // "video/avi"
+ ".flv", ".f4v", // "video/flv"
+ ".mp4", ".m4v", // "video/mp4"
+ ".mov", ".movie", ".qt", // "video/quicktime"
+ ".webm", // "video/webm"
+ ".mpg", ".mpeg", ".ts", ".tsv", ".ps", ".m2ts", ".mts", ".vob", // "video/mpeg"
+ ".ogv", // "video/ogg"
+ ".mkv", ".mks" // "video/matroska"
+ ], StringComparer.OrdinalIgnoreCase);
+ internal static readonly HashSet.AlternateLookup> SupportedVideoExtensionsLookup =
+ SupportedVideoExtensions.GetAlternateLookup>();
+
+ #endregion
+
+ #region Fields
+
+ private bool _isLoaded = true;
+
+ #endregion
+
+ #region Loaders
+
+ private static void PlaceholderSource_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ LayeredBackgroundImage element = (LayeredBackgroundImage)d;
+ if (!element.IsLoaded)
+ {
+ return;
+ }
+
+ Grid grid = element._placeholderGrid;
+ element.LoadFromSourceAsyncDetached(PlaceholderSourceProperty,
+ e.OldValue,
+ nameof(PlaceholderStretch),
+ nameof(PlaceholderHorizontalAlignment),
+ nameof(PlaceholderVerticalAlignment),
+ grid,
+ false,
+ ref element._lastPlaceholderSourceType);
+ }
+
+ private static void BackgroundSource_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ LayeredBackgroundImage element = (LayeredBackgroundImage)d;
+ if (!element.IsLoaded)
+ {
+ return;
+ }
+
+ Grid grid = element._backgroundGrid;
+ element.LoadFromSourceAsyncDetached(BackgroundSourceProperty,
+ e.OldValue,
+ nameof(BackgroundStretch),
+ nameof(BackgroundHorizontalAlignment),
+ nameof(BackgroundVerticalAlignment),
+ grid,
+ true,
+ ref element._lastBackgroundSourceType);
+ }
+
+ private static void ForegroundSource_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ LayeredBackgroundImage element = (LayeredBackgroundImage)d;
+ if (!element.IsLoaded)
+ {
+ return;
+ }
+
+ Grid grid = element._foregroundGrid;
+ element.LoadFromSourceAsyncDetached(ForegroundSourceProperty,
+ e.OldValue,
+ nameof(ForegroundStretch),
+ nameof(ForegroundHorizontalAlignment),
+ nameof(ForegroundVerticalAlignment),
+ grid,
+ false,
+ ref element._lastForegroundSourceType);
+ }
+
+ #endregion
+
+ #region
+
+ private void LoadFromSourceAsyncDetached(
+ DependencyProperty sourceProperty,
+ object? lastSource,
+ string stretchProperty,
+ string horizontalAlignmentProperty,
+ string verticalAlignmentProperty,
+ Grid grid,
+ bool canReceiveVideo,
+ ref MediaSourceType lastMediaType)
+ {
+ try
+ {
+ object? source = GetValue(sourceProperty);
+
+ if (source is null)
+ {
+ goto ClearAndReturnUnknown;
+ }
+
+ if (!TryGetMediaPathFromSource(source, out string? mediaPath))
+ {
+ goto ClearAndReturnUnknown;
+ }
+
+ if (GetMediaSourceTypeFromPath(mediaPath) is var mediaType &&
+ mediaType == MediaSourceType.Unknown)
+ {
+ goto ClearAndReturnUnknown;
+ }
+
+ if (lastMediaType == MediaSourceType.Video)
+ {
+ // Pause and Invalidate Video Player
+ Pause();
+ }
+
+ InnerLoadDetached();
+ lastMediaType = mediaType;
+ return;
+
+ async void InnerLoadDetached()
+ {
+ try
+ {
+ // ReSharper disable once ConvertIfStatementToSwitchStatement
+ if (mediaType == MediaSourceType.Image &&
+ await LoadImageFromSourceAsync(source,
+ stretchProperty,
+ horizontalAlignmentProperty,
+ verticalAlignmentProperty,
+ this,
+ grid))
+ {
+ return;
+ }
+
+ if (mediaType == MediaSourceType.Video &&
+ canReceiveVideo &&
+ await LoadVideoFromSourceAsync(source,
+ lastSource,
+ this))
+ {
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+
+ ClearAndReturnUnknown:
+ ClearMediaGrid(grid);
+ lastMediaType = MediaSourceType.Unknown;
+ }
+
+ private static async ValueTask LoadImageFromSourceAsync(
+ object? source,
+ string stretchProperty,
+ string horizontalAlignmentProperty,
+ string verticalAlignmentProperty,
+ LayeredBackgroundImage instance,
+ Grid grid)
+ {
+ try
+ {
+ // Create instance
+ Image image = new();
+
+ // Bind property
+ image.BindProperty(instance, stretchProperty, Image.StretchProperty, BindingMode.OneWay);
+ image.BindProperty(instance, horizontalAlignmentProperty, HorizontalAlignmentProperty, BindingMode.OneWay);
+ image.BindProperty(instance, verticalAlignmentProperty, VerticalAlignmentProperty, BindingMode.OneWay);
+
+ image.Transitions.Add(new ContentThemeTransition());
+ grid.Children.Add(image);
+
+ image.Tag = (grid, instance);
+ image.ImageOpened += Image_ImageOpened;
+
+ Uri? sourceUri = source as Uri;
+
+ if (sourceUri == null &&
+ source is string asStringSource)
+ {
+ sourceUri = asStringSource.GetStringAsUri();
+ }
+
+ Stream? sourceStream = null;
+ if (source is Stream { CanSeek: true, CanRead: true } asSeekableStream)
+ {
+ sourceStream = asSeekableStream;
+ }
+
+ if (sourceStream == null &&
+ sourceUri == null)
+ {
+ return false;
+ }
+
+ return await image.LoadImageAsync(sourceUri, sourceStream, instance);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return false;
+ }
+ }
+
+ private static async Task LoadVideoFromSourceAsync(
+ object? source,
+ object? lastSource,
+ LayeredBackgroundImage instance)
+ {
+ bool isLastStreamSame = IsSourceKindEquals(source, lastSource);
+
+ if (instance.MediaCacheHandler is { } cacheHandler)
+ {
+ MediaCacheResult cacheResult = await cacheHandler.LoadCachedSource(source);
+ source = cacheResult.CachedSource;
+ }
+
+ Uri? sourceUri = source as Uri;
+
+ if (sourceUri == null &&
+ source is string asStringSource)
+ {
+ sourceUri = asStringSource.GetStringAsUri();
+ }
+
+ Stream? sourceStream = null;
+ if (source is Stream { CanSeek: true, CanRead: true } asSeekableStream)
+ {
+ sourceStream = asSeekableStream;
+ }
+
+ if (sourceStream == null &&
+ sourceUri == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ // Set-ups Video Player upfront
+ instance.InitializeVideoPlayer();
+
+ // Assign media source
+ if (sourceStream != null)
+ {
+ IRandomAccessStream? sourceStreamRandom = sourceStream.AsRandomAccessStream();
+ instance._videoPlayer.SetStreamSource(sourceStreamRandom);
+ }
+ else if (sourceUri != null)
+ {
+ instance._videoPlayer.SetUriSource(sourceUri);
+ }
+ else
+ {
+ return false;
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return false;
+ }
+
+ // Seek to last position if source was the same
+ if (isLastStreamSame &&
+ instance._videoPlayer.CanSeek)
+ {
+ instance._videoPlayer.Position = instance._lastVideoPlayerPosition;
+ }
+
+ return true;
+ }
+
+ private void InitializeVideoFrameOnMediaOpened(MediaPlayer sender, object args)
+ {
+ DispatcherQueue.TryEnqueue(Impl);
+ return;
+
+ void Impl()
+ {
+ // Create instance
+ Image image = new()
+ {
+ Tag = (_backgroundGrid, this),
+ Name = "VideoRenderFrame"
+ };
+
+ // Bind property
+ image.BindProperty(this,
+ nameof(BackgroundStretch),
+ Image.StretchProperty,
+ BindingMode.OneWay);
+ image.BindProperty(this,
+ nameof(BackgroundHorizontalAlignment),
+ HorizontalAlignmentProperty,
+ BindingMode.OneWay);
+ image.BindProperty(this,
+ nameof(BackgroundVerticalAlignment),
+ VerticalAlignmentProperty,
+ BindingMode.OneWay);
+
+ InitializeRenderTargetSize(sender.PlaybackSession);
+
+ // Register events
+ image.Loaded += Image_VideoFrameOnLoaded;
+ image.Unloaded += Image_VideoFrameOnUnloaded;
+
+ // Add to children
+ image.Transitions.Add(new ContentThemeTransition());
+ _backgroundGrid.Children.Add(image);
+ }
+ }
+
+ private static void Image_VideoFrameOnLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Image { Tag: ValueTuple parentGrid })
+ {
+ return;
+ }
+
+ parentGrid.Item2.Play();
+ Image_ImageOpened(sender, e);
+ }
+
+ private static void Image_VideoFrameOnUnloaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Image { Tag: ValueTuple parentGrid })
+ {
+ return;
+ }
+
+ parentGrid.Item2.Pause();
+ }
+
+ private static void Image_ImageOpened(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Image { Tag: ValueTuple parentGrid } image)
+ {
+ return;
+ }
+
+ // Set placeholder to hidden once loaded
+ ref bool isPlaceholderHidden = ref parentGrid.Item2._isPlaceholderHidden;
+ if (parentGrid.Item1.Name.StartsWith("Background", StringComparison.OrdinalIgnoreCase) &&
+ !Interlocked.Exchange(ref isPlaceholderHidden, true))
+ {
+ VisualStateManager.GoToState(parentGrid.Item2, StateNamePlaceholderStateHidden, true);
+ }
+
+ // HACK: Tells the Grid to temporarily detach all UIElement children
+ // then re-add the image to the grid
+ ClearMediaGrid(parentGrid.Item1, image);
+
+ // Remove transition once loaded
+ image.Transitions.Clear();
+ }
+
+ private static MediaSourceType GetMediaSourceTypeFromPath(ReadOnlySpan path)
+ {
+ ReadOnlySpan extension = Path.GetExtension(path);
+
+ if (SupportedImageBitmapExtensionsLookup.Contains(extension) ||
+ SupportedImageBitmapExternalCodecExtensionsLookup.Contains(extension) ||
+ SupportedImageVectorExtensionsLookup.Contains(extension))
+ {
+ return MediaSourceType.Image;
+ }
+
+ if (SupportedVideoExtensionsLookup.Contains(extension))
+ {
+ return MediaSourceType.Video;
+ }
+
+ return MediaSourceType.Unknown;
+ }
+
+ // Returns true if the media source is supported. Otherwise, false and return a null string path.
+ private static bool TryGetMediaPathFromSource(object? source, [NotNullWhen(true)] out string? path)
+ {
+ Unsafe.SkipInit(out path);
+
+ path = source switch
+ {
+ string asString => asString,
+ Uri asUrl => asUrl.AbsolutePath,
+ FileInfo asFileInfo => asFileInfo.FullName,
+ FileStream asFileStream => asFileStream.Name,
+ _ => path
+ };
+
+ return !string.IsNullOrEmpty(path);
+ }
+
+ private static void ClearMediaGrid(Grid grid, UIElement? except = null)
+ {
+ List elementExcepted =
+ except == null ? grid.Children.ToList() :
+ grid.Children.Where(x => x != except)
+ .ToList();
+
+ foreach (Image image in elementExcepted.OfType())
+ {
+ // This one is for Image. The source will always guarantee to call this event.
+ image.ImageOpened -= Image_ImageOpened;
+ // This one is for Video since ImageOpened with Canvas source will never trigger this so we use Loaded instead.
+ image.Loaded -= Image_VideoFrameOnLoaded;
+ image.Unloaded -= Image_VideoFrameOnUnloaded;
+ // Clears the loaded ImageSource
+ image.Source = null;
+ }
+
+ foreach (UIElement element in elementExcepted)
+ {
+ grid.Children.Remove(element);
+ }
+ }
+
+#endregion
+}
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.cs
new file mode 100644
index 000000000..4d7f70bc2
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.cs
@@ -0,0 +1,277 @@
+using Microsoft.UI.Composition;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Input;
+using System;
+using System.Numerics;
+using System.Threading;
+using Windows.Foundation;
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+
+public partial class LayeredBackgroundImage
+{
+ #region Fields
+
+ private object? _lastPlaceholderSource;
+ private object? _lastBackgroundSource;
+ private object? _lastForegroundSource;
+
+ private MediaSourceType _lastPlaceholderSourceType;
+ private MediaSourceType _lastBackgroundSourceType;
+ private MediaSourceType _lastForegroundSourceType;
+
+ private bool _isPlaceholderHidden;
+
+ #endregion
+
+ #region Loaded and Unloaded
+
+ private static bool IsSourceKindEquals(object? left, object? right)
+ {
+ if (left is string asStringLeft && right is string asStringRight)
+ {
+ return string.Equals(asStringLeft, asStringRight, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return left == right;
+ }
+
+ private void LayeredBackgroundImage_OnLoaded(object sender, RoutedEventArgs e)
+ {
+ Interlocked.Exchange(ref _isLoaded, true);
+ ParallaxView_ToggleEnable(IsParallaxEnabled);
+ ParallaxGrid_OnUpdateCenterPoint();
+
+ if (!_isPlaceholderHidden &&
+ !IsSourceKindEquals(_lastPlaceholderSource, PlaceholderSource))
+ {
+ LoadFromSourceAsyncDetached(PlaceholderSourceProperty,
+ _lastPlaceholderSource,
+ nameof(PlaceholderStretch),
+ nameof(PlaceholderHorizontalAlignment),
+ nameof(PlaceholderVerticalAlignment),
+ _placeholderGrid,
+ false,
+ ref _lastPlaceholderSourceType);
+ _lastPlaceholderSource = PlaceholderSource;
+ }
+
+ if (!IsSourceKindEquals(_lastBackgroundSource, BackgroundSource))
+ {
+ LoadFromSourceAsyncDetached(BackgroundSourceProperty,
+ _lastBackgroundSource,
+ nameof(BackgroundStretch),
+ nameof(BackgroundHorizontalAlignment),
+ nameof(BackgroundVerticalAlignment),
+ _backgroundGrid,
+ true,
+ ref _lastBackgroundSourceType);
+ _lastBackgroundSource = BackgroundSource;
+ }
+
+ // ReSharper disable once InvertIf
+ if (!IsSourceKindEquals(_lastForegroundSource, ForegroundSource))
+ {
+ LoadFromSourceAsyncDetached(ForegroundSourceProperty,
+ _lastForegroundSource,
+ nameof(ForegroundStretch),
+ nameof(ForegroundHorizontalAlignment),
+ nameof(ForegroundVerticalAlignment),
+ _foregroundGrid,
+ false,
+ ref _lastForegroundSourceType);
+ _lastForegroundSource = ForegroundSource;
+ }
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ private void LayeredBackgroundImage_OnUnloaded(object sender, RoutedEventArgs e)
+ {
+ Interlocked.Exchange(ref _isLoaded, false);
+ ParallaxGrid_UnregisterEffect();
+ _lastParallaxHoverSource = null;
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ #endregion
+
+ #region Parallax View
+
+ private static void IsParallaxEnabled_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ LayeredBackgroundImage element = (LayeredBackgroundImage)d;
+ if (!element.IsLoaded)
+ {
+ return;
+ }
+
+ element.ParallaxView_ToggleEnable(element.IsParallaxEnabled);
+ }
+
+ private static void ParallaxHover_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ LayeredBackgroundImage element = (LayeredBackgroundImage)d;
+ if (!element.IsLoaded)
+ {
+ return;
+ }
+
+ element.ParallaxGrid_RegisterPointerEvents(); // Re-register hover event
+ }
+
+ private void ParallaxView_ToggleEnable(bool isEnable)
+ {
+ if (isEnable)
+ {
+ ParallaxGrid_RegisterEffect();
+ return;
+ }
+
+ ParallaxGrid_UnregisterEffect();
+ }
+
+ private void ParallaxGrid_RegisterEffect()
+ {
+ ParallaxGrid_RegisterPointerEvents();
+ ParallaxGrid_OffsetReset();
+
+ _parallaxGrid.SizeChanged += ParallaxGrid_OnSizeChanged;
+ }
+
+ private void ParallaxGrid_UnregisterEffect()
+ {
+ ParallaxGrid_UnregisterPointerEvents();
+ ParallaxGrid_OffsetReset();
+
+ _parallaxGrid.SizeChanged -= ParallaxGrid_OnSizeChanged;
+ }
+
+ private void ParallaxGrid_RegisterPointerEvents()
+ {
+ UIElement eventSource = ParallaxGrid_UnregisterPointerEvents();
+
+ eventSource.PointerMoved += ParallaxGrid_OnPointerMoved;
+ eventSource.PointerEntered += ParallaxGrid_OnPointerEntered;
+ eventSource.PointerExited += ParallaxGrid_OnPointerExited;
+ _lastParallaxHoverSource = eventSource;
+ }
+
+ private UIElement ParallaxGrid_UnregisterPointerEvents()
+ {
+ UIElement eventSource = ParallaxHoverSource ?? _parallaxGrid;
+ if (_lastParallaxHoverSource != null)
+ {
+ _lastParallaxHoverSource.PointerMoved -= ParallaxGrid_OnPointerMoved;
+ _lastParallaxHoverSource.PointerEntered -= ParallaxGrid_OnPointerEntered;
+ _lastParallaxHoverSource.PointerExited -= ParallaxGrid_OnPointerExited;
+ }
+
+ eventSource.PointerMoved -= ParallaxGrid_OnPointerMoved;
+ eventSource.PointerEntered -= ParallaxGrid_OnPointerEntered;
+ eventSource.PointerExited -= ParallaxGrid_OnPointerExited;
+ return eventSource;
+ }
+
+ private void ParallaxGrid_OnSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ ParallaxGrid_OnUpdateCenterPoint();
+ }
+
+ private void ParallaxGrid_OnUpdateCenterPoint()
+ {
+ if (!IsLoaded)
+ {
+ return;
+ }
+
+ _parallaxGridVisual.CenterPoint = new Vector3((float)_parallaxGrid.RenderSize.Width / 2,
+ (float)_parallaxGrid.RenderSize.Height / 2,
+ 0);
+ }
+
+ private void ParallaxGrid_OnPointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ ParallaxGrid_OffsetReset();
+ }
+
+ private void ParallaxGrid_OnPointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ ParallaxGrid_OnPointerMoved(sender, e);
+ }
+
+ private void ParallaxGrid_OffsetReset()
+ {
+ ParallaxGrid_StartAnimation(Vector3.Zero, Vector3.One, 250d);
+ }
+
+ private void ParallaxGrid_OnPointerMoved(object sender, PointerRoutedEventArgs e)
+ {
+ FrameworkElement element = (FrameworkElement)sender;
+
+ double offsetX = ParallaxHorizontalShift;
+ double offsetY = ParallaxVerticalShift;
+
+ // Move
+ Point pos = e.GetCurrentPoint(element).Position;
+ double w = element.ActualWidth;
+ double h = element.ActualHeight;
+
+ if (w <= 0 || h <= 0)
+ return;
+
+ // Normalize mouse position to range [-1, +1]
+ double nx = pos.X / w * 2d - 1d;
+ double ny = pos.Y / h * 2d - 1d;
+
+ // Move opposite to pointer
+ float tx = (float)(-nx * offsetX);
+ float ty = (float)(-ny * offsetY);
+
+ // Scale with x2 as it counts with each side of axis (Left, Right) (Top, Bottom)
+ Vector2 size = _parallaxGrid.ActualSize;
+ double sizeToX = size.X + Math.Abs(offsetX) * 2;
+ double sizeToY = size.Y + Math.Abs(offsetY) * 2;
+
+ double addScaleX = sizeToX / size.X;
+ double addScaleY = sizeToY / size.Y;
+
+ // Gets the stronger axis
+ float factorScale = (float)Math.Max(addScaleX, addScaleY);
+
+ ParallaxGrid_StartAnimation(Vector3.Zero with { X = tx, Y = ty },
+ Vector3.One with { X = factorScale, Y = factorScale },
+ 40);
+ }
+
+ private void ParallaxGrid_StartAnimation(Vector3 offset, Vector3 scale, double duration)
+ {
+ const string targetTranslation = "Translation";
+ const string targetScale = "Scale";
+
+ CompositionAnimationGroup? animGroup = _parallaxGridCompositor.CreateAnimationGroup();
+
+ // Move
+ Vector3KeyFrameAnimation? anim = _parallaxGridCompositor.CreateVector3KeyFrameAnimation();
+ anim.Duration = TimeSpan.FromMilliseconds(duration);
+ anim.InsertKeyFrame(1f, offset);
+ anim.Target = targetTranslation;
+
+ // Scale
+ Vector3KeyFrameAnimation? scaleAnim = _parallaxGridCompositor.CreateVector3KeyFrameAnimation();
+ scaleAnim.Duration = TimeSpan.FromMilliseconds(duration);
+ scaleAnim.InsertKeyFrame(1f, scale);
+ scaleAnim.Target = targetScale;
+
+ animGroup.Add(anim);
+ animGroup.Add(scaleAnim);
+
+ _parallaxGridVisual.StartAnimationGroup(animGroup);
+ }
+
+ #endregion
+}
+
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Properties.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Properties.cs
new file mode 100644
index 000000000..4879a97f2
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Properties.cs
@@ -0,0 +1,249 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+
+public partial class LayeredBackgroundImage
+{
+ #region Properties
+
+ public IMediaCacheHandler? MediaCacheHandler
+ {
+ get => (IMediaCacheHandler?)GetValue(MediaCacheHandlerProperty);
+ set => SetValue(MediaCacheHandlerProperty, value);
+ }
+
+ public bool IsAudioEnabled
+ {
+ get => (bool)GetValue(IsAudioEnabledProperty);
+ set => SetValue(IsAudioEnabledProperty, value);
+ }
+
+ public double AudioVolume
+ {
+ get => GetValue(AudioVolumeProperty).TryGetDouble();
+ set => SetValue(AudioVolumeProperty, value);
+ }
+
+ public bool IsParallaxEnabled
+ {
+ get => (bool)GetValue(IsParallaxEnabledProperty);
+ set => SetValue(IsParallaxEnabledProperty, value);
+ }
+
+ public double ParallaxHorizontalShift
+ {
+ get => (double)GetValue(ParallaxHorizontalShiftProperty);
+ set => SetValue(ParallaxHorizontalShiftProperty, value);
+ }
+
+ public double ParallaxVerticalShift
+ {
+ get => (double)GetValue(ParallaxVerticalShiftProperty);
+ set => SetValue(ParallaxVerticalShiftProperty, value);
+ }
+
+ public UIElement? ParallaxHoverSource
+ {
+ get => (UIElement?)GetValue(ParallaxHoverSourceProperty);
+ set => SetValue(ParallaxHoverSourceProperty, value);
+ }
+
+ public object? PlaceholderSource
+ {
+ get => (object?)GetValue(PlaceholderSourceProperty);
+ set => SetValue(PlaceholderSourceProperty, value);
+ }
+
+ public Stretch PlaceholderStretch
+ {
+ get => (Stretch)GetValue(PlaceholderStretchProperty);
+ set => SetValue(PlaceholderStretchProperty, value);
+ }
+
+ public HorizontalAlignment PlaceholderHorizontalAlignment
+ {
+ get => (HorizontalAlignment)GetValue(PlaceholderHorizontalAlignmentProperty);
+ set => SetValue(PlaceholderHorizontalAlignmentProperty, value);
+ }
+
+ public VerticalAlignment PlaceholderVerticalAlignment
+ {
+ get => (VerticalAlignment)GetValue(PlaceholderVerticalAlignmentProperty);
+ set => SetValue(PlaceholderVerticalAlignmentProperty, value);
+ }
+
+ public object? BackgroundSource
+ {
+ get => (object?)GetValue(BackgroundSourceProperty);
+ set => SetValue(BackgroundSourceProperty, value);
+ }
+
+ public Stretch BackgroundStretch
+ {
+ get => (Stretch)GetValue(BackgroundStretchProperty);
+ set => SetValue(BackgroundStretchProperty, value);
+ }
+
+ public HorizontalAlignment BackgroundHorizontalAlignment
+ {
+ get => (HorizontalAlignment)GetValue(BackgroundHorizontalAlignmentProperty);
+ set => SetValue(BackgroundHorizontalAlignmentProperty, value);
+ }
+
+ public VerticalAlignment BackgroundVerticalAlignment
+ {
+ get => (VerticalAlignment)GetValue(BackgroundVerticalAlignmentProperty);
+ set => SetValue(BackgroundVerticalAlignmentProperty, value);
+ }
+
+ public object? ForegroundSource
+ {
+ get => (object?)GetValue(ForegroundSourceProperty);
+ set => SetValue(ForegroundSourceProperty, value);
+ }
+
+ public Stretch ForegroundStretch
+ {
+ get => (Stretch)GetValue(ForegroundStretchProperty);
+ set => SetValue(ForegroundStretchProperty, value);
+ }
+
+ public HorizontalAlignment ForegroundHorizontalAlignment
+ {
+ get => (HorizontalAlignment)GetValue(ForegroundHorizontalAlignmentProperty);
+ set => SetValue(ForegroundHorizontalAlignmentProperty, value);
+ }
+
+ public VerticalAlignment ForegroundVerticalAlignment
+ {
+ get => (VerticalAlignment)GetValue(ForegroundVerticalAlignmentProperty);
+ set => SetValue(ForegroundVerticalAlignmentProperty, value);
+ }
+
+ #endregion
+
+ #region Fields
+
+ private UIElement? _lastParallaxHoverSource;
+
+ #endregion
+
+ #region Dependency Properties
+
+ public static readonly DependencyProperty MediaCacheHandlerProperty =
+ DependencyProperty.Register(nameof(MediaCacheHandler),
+ typeof(IMediaCacheHandler),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(null!));
+
+ public static readonly DependencyProperty IsAudioEnabledProperty =
+ DependencyProperty.Register(nameof(IsAudioEnabled),
+ typeof(bool),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(false, IsAudioEnabled_OnChange));
+
+ public static readonly DependencyProperty AudioVolumeProperty =
+ DependencyProperty.Register(nameof(AudioVolume),
+ typeof(double),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(50d, AudioVolume_OnChange));
+
+ public static readonly DependencyProperty IsParallaxEnabledProperty =
+ DependencyProperty.Register(nameof(IsParallaxEnabled),
+ typeof(bool),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(false, IsParallaxEnabled_OnChange));
+
+ public static readonly DependencyProperty ParallaxHorizontalShiftProperty =
+ DependencyProperty.Register(nameof(ParallaxHorizontalShift),
+ typeof(double),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(32d));
+
+ public static readonly DependencyProperty ParallaxVerticalShiftProperty =
+ DependencyProperty.Register(nameof(ParallaxVerticalShift),
+ typeof(double),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(32d));
+
+ public static readonly DependencyProperty ParallaxHoverSourceProperty =
+ DependencyProperty.Register(nameof(ParallaxHoverSource),
+ typeof(UIElement),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(null!, ParallaxHover_OnChange));
+
+ public static readonly DependencyProperty PlaceholderSourceProperty =
+ DependencyProperty.Register(nameof(PlaceholderSource),
+ typeof(object),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(null!, PlaceholderSource_OnChange));
+
+ public static readonly DependencyProperty PlaceholderStretchProperty =
+ DependencyProperty.Register(nameof(PlaceholderStretch),
+ typeof(Stretch),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(Stretch.UniformToFill));
+
+ public static readonly DependencyProperty PlaceholderHorizontalAlignmentProperty =
+ DependencyProperty.Register(nameof(PlaceholderHorizontalAlignment),
+ typeof(HorizontalAlignment),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(HorizontalAlignment.Center));
+
+ public static readonly DependencyProperty PlaceholderVerticalAlignmentProperty =
+ DependencyProperty.Register(nameof(PlaceholderVerticalAlignment),
+ typeof(VerticalAlignment),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(VerticalAlignment.Center));
+
+ public static readonly DependencyProperty BackgroundSourceProperty =
+ DependencyProperty.Register(nameof(BackgroundSource),
+ typeof(object),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(null!, BackgroundSource_OnChange));
+
+ public static readonly DependencyProperty BackgroundStretchProperty =
+ DependencyProperty.Register(nameof(BackgroundStretch),
+ typeof(Stretch),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(Stretch.UniformToFill));
+
+ public static readonly DependencyProperty BackgroundHorizontalAlignmentProperty =
+ DependencyProperty.Register(nameof(BackgroundHorizontalAlignment),
+ typeof(HorizontalAlignment),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(HorizontalAlignment.Center));
+
+ public static readonly DependencyProperty BackgroundVerticalAlignmentProperty =
+ DependencyProperty.Register(nameof(BackgroundVerticalAlignment),
+ typeof(VerticalAlignment),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(VerticalAlignment.Center));
+
+ public static readonly DependencyProperty ForegroundSourceProperty =
+ DependencyProperty.Register(nameof(ForegroundSource),
+ typeof(object),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(null!, ForegroundSource_OnChange));
+
+ public static readonly DependencyProperty ForegroundStretchProperty =
+ DependencyProperty.Register(nameof(ForegroundStretch),
+ typeof(Stretch),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(Stretch.UniformToFill));
+
+ public static readonly DependencyProperty ForegroundHorizontalAlignmentProperty =
+ DependencyProperty.Register(nameof(ForegroundHorizontalAlignment),
+ typeof(HorizontalAlignment),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(HorizontalAlignment.Center));
+
+ public static readonly DependencyProperty ForegroundVerticalAlignmentProperty =
+ DependencyProperty.Register(nameof(ForegroundVerticalAlignment),
+ typeof(VerticalAlignment),
+ typeof(LayeredBackgroundImage),
+ new PropertyMetadata(VerticalAlignment.Center));
+
+ #endregion
+}
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Templates.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Templates.cs
new file mode 100644
index 000000000..e09ae086f
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Templates.cs
@@ -0,0 +1,75 @@
+using Hi3Helper.Win32.Native.Interfaces.DXGI;
+using Hi3Helper.Win32.WinRT.SwapChainPanelHelper;
+using Microsoft.Graphics.Canvas;
+using Microsoft.UI.Composition;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Hosting;
+using Microsoft.UI.Xaml.Media.Imaging;
+using System.Threading;
+using Windows.Media.Playback;
+
+using CanvasRect = Hi3Helper.Win32.Native.Structs.Rect;
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+public partial class LayeredBackgroundImage
+{
+ #region Constants
+
+ private const string TemplateNameParallaxGrid = "ParallaxGrid";
+ private const string TemplateNamePlaceholderGrid = "PlaceholderGrid";
+ private const string TemplateNameBackgroundGrid = "BackgroundGrid";
+ private const string TemplateNameForegroundGrid = "ForegroundGrid";
+
+ private const string StateNamePlaceholderStateHidden = "PlaceholderStateHidden";
+
+ #endregion
+
+ #region Fields
+
+ private Grid _parallaxGrid = null!;
+ private Grid _placeholderGrid = null!;
+ private Grid _backgroundGrid = null!;
+ private Grid _foregroundGrid = null!;
+
+ private Visual _parallaxGridVisual = null!;
+ private Compositor _parallaxGridCompositor = null!;
+
+ private D3DDeviceContext? _canvasD3DDeviceContext;
+ private ISurfaceImageSourceNativeWithD2D? _canvasSurfaceImageSourceNative;
+ private nint _canvasSurfaceImageSourceNativePtr = nint.Zero;
+ private SurfaceImageSource? _canvasSurfaceImageSource;
+
+ private CanvasDevice? _canvasDevice = null!;
+ private int _canvasWidth;
+ private int _canvasHeight;
+ private CanvasRect _canvasRenderArea;
+
+ private MediaPlayer _videoPlayer = null!;
+ private nint _videoPlayerPtr = nint.Zero;
+
+ private bool _isTemplateLoaded;
+
+ #endregion
+
+ #region Apply Template Methods
+
+ protected override void OnApplyTemplate()
+ {
+ _parallaxGrid = this.GetTemplateChild(TemplateNameParallaxGrid);
+ _placeholderGrid = this.GetTemplateChild(TemplateNamePlaceholderGrid);
+ _backgroundGrid = this.GetTemplateChild(TemplateNameBackgroundGrid);
+ _foregroundGrid = this.GetTemplateChild(TemplateNameForegroundGrid);
+
+ ElementCompositionPreview.SetIsTranslationEnabled(_parallaxGrid, true);
+
+ _parallaxGridVisual = ElementCompositionPreview.GetElementVisual(_parallaxGrid);
+ _parallaxGridCompositor = _parallaxGridVisual.Compositor;
+
+ Interlocked.Exchange(ref _isTemplateLoaded, true);
+
+ base.OnApplyTemplate();
+ }
+
+ #endregion
+}
+
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.cs
new file mode 100644
index 000000000..99aefe0f6
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.cs
@@ -0,0 +1,23 @@
+using Microsoft.UI.Xaml.Controls;
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+
+public partial class LayeredBackgroundImage : Control
+{
+ public LayeredBackgroundImage()
+ {
+ Loaded += LayeredBackgroundImage_OnLoaded;
+ Unloaded += LayeredBackgroundImage_OnUnloaded;
+
+ DefaultStyleKey = typeof(LayeredBackgroundImage);
+ }
+
+ ~LayeredBackgroundImage()
+ {
+ Loaded -= LayeredBackgroundImage_OnLoaded;
+ Unloaded -= LayeredBackgroundImage_OnUnloaded;
+
+ DisposeRenderTarget();
+ DisposeVideoPlayer();
+ }
+}
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.xaml b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.xaml
new file mode 100644
index 000000000..83e31a0a9
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImageExtensions.cs b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImageExtensions.cs
new file mode 100644
index 000000000..fafdfe679
--- /dev/null
+++ b/BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImageExtensions.cs
@@ -0,0 +1,300 @@
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media.Imaging;
+using PhotoSauce.MagicScaler;
+using PhotoSauce.MagicScaler.Transforms;
+using PhotoSauce.NativeCodecs.Libheif;
+using PhotoSauce.NativeCodecs.Libjxl;
+using PhotoSauce.NativeCodecs.Libwebp;
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Windows.Storage.Streams;
+// ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault
+// ReSharper disable ConvertIfStatementToSwitchStatement
+
+namespace BackgroundTest.CustomControl.LayeredBackgroundImage;
+
+internal static class LayeredBackgroundImageExtensions
+{
+ private static readonly HttpClient Client;
+
+ static LayeredBackgroundImageExtensions()
+ {
+ // Initialize support for MagicScaler's WebP decoding
+ CodecManager.Configure(InitializeMagicScalerCodecs);
+ Client = CreateHttpClient();
+ }
+
+ private static void InitializeMagicScalerCodecs(CodecCollection codecs)
+ {
+ codecs.UseWicCodecs(WicCodecPolicy.All);
+ codecs.UseLibwebp();
+ codecs.UseLibheif();
+ codecs.UseLibjxl();
+ }
+
+ private static HttpClient CreateHttpClient()
+ {
+ SocketsHttpHandler handler = new()
+ {
+ AllowAutoRedirect = true,
+ MaxConnectionsPerServer = Environment.ProcessorCount * 2
+ };
+ return new HttpClient(handler, false);
+ }
+
+ extension(Image image)
+ {
+ internal async ValueTask LoadImageAsync(Uri? sourceFromPath,
+ Stream? sourceFromStream,
+ LayeredBackgroundImage instance,
+ IPixelTransform? pixelTransform = null)
+ {
+ IMediaCacheHandler? cacheHandler = instance.MediaCacheHandler;
+ bool isDisposeStream = sourceFromStream == null;
+
+ if (cacheHandler != null)
+ {
+ object? source = sourceFromPath;
+ source ??= sourceFromStream;
+ MediaCacheResult cacheResult = await cacheHandler.LoadCachedSource(source);
+
+ if (cacheResult == null!)
+ {
+ return false;
+ }
+
+ Uri? cachedUrlSource = cacheResult.CachedSource as Uri;
+ Stream? cachedStreamSource = null;
+
+ if (cacheResult.CachedSource is string asString)
+ {
+ cachedUrlSource = asString.GetStringAsUri();
+ }
+
+ if (cacheResult.CachedSource is Stream asStream)
+ {
+ cachedStreamSource = asStream;
+ }
+
+ isDisposeStream = cacheResult.DisposeStream;
+
+ if (cacheResult.ForceUseInternalDecoder &&
+ await image.LoadImageWithInternalDecoderAsync(sourceFromPath,
+ sourceFromStream,
+ true,
+ isDisposeStream))
+ {
+ return true;
+ }
+
+ sourceFromPath = cachedUrlSource;
+ sourceFromStream = cachedStreamSource;
+ }
+
+ if (pixelTransform == null &&
+ await image.LoadImageWithInternalDecoderAsync(sourceFromPath,
+ sourceFromStream))
+ {
+ return true;
+ }
+
+ Stream? sourceStream = sourceFromStream ?? await TryGetStreamFromPathAsync(sourceFromPath);
+ if (sourceStream is not { CanSeek: true, CanRead: true })
+ {
+ return false;
+ }
+
+ try
+ {
+ string temporaryFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
+ await using FileStream decodedBitmapStream =
+ File.Open(temporaryFilePath,
+ new FileStreamOptions
+ {
+ Mode = FileMode.Create,
+ Access = FileAccess.ReadWrite,
+ Share = FileShare.ReadWrite,
+ Options = FileOptions.DeleteOnClose
+ });
+
+ await Task.Run(() =>
+ {
+ using ProcessingPipeline pipeline =
+ MagicImageProcessor.BuildPipeline(sourceStream, ProcessImageSettings.Default);
+
+ // For adding Waifu2X transform support later.
+ if (pixelTransform != null)
+ {
+ pipeline.AddTransform(pixelTransform);
+ }
+
+ pipeline.WriteOutput(decodedBitmapStream);
+ });
+ using IRandomAccessStream randomStream = decodedBitmapStream.AsRandomAccessStream();
+
+ decodedBitmapStream.Position = 0;
+ BitmapImage bitmapImage = new();
+ image.Source = bitmapImage;
+
+ await bitmapImage.SetSourceAsync(randomStream);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ return false;
+ }
+ finally
+ {
+ if (isDisposeStream)
+ {
+ await sourceStream.DisposeAsync();
+ }
+ }
+ }
+
+ private async ValueTask LoadImageWithInternalDecoderAsync(Uri? sourceFromPath,
+ Stream? sourceFromStream,
+ bool force = false,
+ bool disposeStream = false)
+ {
+ string? filePath = sourceFromPath?.AbsolutePath;
+ filePath ??= (sourceFromStream as FileStream)?.Name;
+
+ if (string.IsNullOrEmpty(filePath))
+ {
+ return false;
+ }
+
+ string extension = Path.GetExtension(filePath);
+ if (!force &&
+ LayeredBackgroundImage.SupportedImageBitmapExternalCodecExtensionsLookup
+ .Contains(extension))
+ {
+ return false;
+ }
+
+ bool isDisposeStream = disposeStream;
+ Stream? sourceStream = sourceFromStream ?? await TryGetStreamFromPathAsync(sourceFromPath);
+ if (sourceStream is not { CanSeek: true, CanRead: true })
+ {
+ return false;
+ }
+
+ try
+ {
+ IRandomAccessStream randomStream = sourceStream.AsRandomAccessStream();
+ BitmapImage bitmapImage = new();
+ image.Source = bitmapImage;
+
+ await bitmapImage.SetSourceAsync(randomStream);
+ if (isDisposeStream)
+ {
+ randomStream.Dispose();
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ return false;
+ }
+ finally
+ {
+ if (isDisposeStream)
+ {
+ await sourceStream.DisposeAsync();
+ }
+ }
+ }
+ }
+
+ private static async Task TryGetStreamFromPathAsync(Uri? url, long? minLengthRequested = null)
+ {
+ if (url == null)
+ {
+ return null;
+ }
+
+ if (url.IsFile)
+ {
+ return File.Open(url.LocalPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ }
+
+ if (await GetFileLengthFromUrl(Client, url) is var fileLength &&
+ fileLength == 0)
+ {
+ return null;
+ }
+
+ HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
+ HttpResponseMessage responseMessage =
+ await Client.SendAsync(requestMessage,
+ HttpCompletionOption.ResponseHeadersRead);
+
+ if (!responseMessage.IsSuccessStatusCode)
+ {
+ responseMessage.Dispose();
+ requestMessage.Dispose();
+ return null;
+ }
+
+ minLengthRequested ??= fileLength;
+
+ try
+ {
+ int writtenLength = (int)minLengthRequested;
+ byte[] requestedBuffer = GC.AllocateUninitializedArray(writtenLength);
+ await using Stream networkStream = await responseMessage.Content.ReadAsStreamAsync();
+
+ Memory buffer = requestedBuffer;
+ while (writtenLength > 0)
+ {
+ int read = await networkStream.ReadAsync(buffer);
+ if (read == 0)
+ {
+ break;
+ }
+ writtenLength -= read;
+ buffer = buffer[read..];
+ }
+
+ return new MemoryStream(requestedBuffer);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+
+ return null;
+ }
+
+ private static async Task GetFileLengthFromUrl(HttpClient client, Uri url)
+ {
+ HttpRequestMessage requestMessage = new(HttpMethod.Head, url);
+ HttpResponseMessage responseMessage =
+ await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
+
+ if (responseMessage.IsSuccessStatusCode)
+ {
+ return responseMessage.Content.Headers.ContentLength ?? 0;
+ }
+
+ responseMessage.Dispose();
+ requestMessage.Dispose();
+ return 0;
+ }
+
+ internal static double GetClampedVolume(this double thisVolume)
+ {
+ if (double.IsNaN(thisVolume) ||
+ double.IsInfinity(thisVolume))
+ {
+ thisVolume = 0;
+ }
+
+ return Math.Clamp(thisVolume, 0, 100d) / 100d;
+ }
+}
diff --git a/BackgroundTest/CustomControl/ManagedUIElementList.cs b/BackgroundTest/CustomControl/ManagedUIElementList.cs
new file mode 100644
index 000000000..acda8c9a0
--- /dev/null
+++ b/BackgroundTest/CustomControl/ManagedUIElementList.cs
@@ -0,0 +1,215 @@
+using Microsoft.UI.Xaml;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using WinRT;
+
+// ReSharper disable StaticMemberInGenericType
+
+namespace BackgroundTest.CustomControl;
+
+[ProjectedRuntimeClass(typeof(IList))]
+[GeneratedBindableCustomProperty]
+public partial class ManagedUIElementList : IList, INotifyCollectionChanged, INotifyPropertyChanged
+{
+ private readonly List _backedList;
+
+ #region Cached Property Changes Arguments
+
+ private static readonly PropertyChangedEventArgs CountPropertyChanged = new(nameof(Count));
+ private static readonly PropertyChangedEventArgs IndexerPropertyChanged = new("Item[]");
+
+ #endregion
+
+ #region Events
+
+ ///
+ public event NotifyCollectionChangedEventHandler? CollectionChanged;
+
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ #endregion
+
+ ///
+ /// Creates a new empty Observable List
+ ///
+ public ManagedUIElementList() => _backedList = [];
+
+ ///
+ /// Creates a new Observable List from the enumerable.
+ ///
+ ///
+ /// Borrow the current instance of instead of allocating new backed list if possible.
+ public ManagedUIElementList(IEnumerable enumerable, bool useBorrow = false)
+ {
+ if (enumerable is List borrowedList &&
+ useBorrow)
+ {
+ _backedList = borrowedList;
+ return;
+ }
+
+ _backedList = [.. enumerable];
+ }
+
+ ///
+ public IEnumerator GetEnumerator() => _backedList.GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public void Add(UIElement item) => Add(item, true);
+
+ ///
+ /// Adds an item to the backed List and notify the changes.
+ ///
+ /// The object to add to the backed List.
+ /// Whether to notify the changes or not.
+ public void Add(UIElement item, bool notifyChanges)
+ {
+ _backedList.Add(item);
+ if (!notifyChanges)
+ {
+ return;
+ }
+
+ NotifyCountPropertyChange();
+ NotifyIndexerPropertyChange();
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
+ }
+
+ ///
+ public void Clear() => Clear(true);
+
+ ///
+ /// Removes all items from the backed List and notify the changes.
+ ///
+ /// Whether to notify the changes or not.
+ public void Clear(bool notifyChanges)
+ {
+ _backedList.Clear();
+ if (!notifyChanges)
+ {
+ return;
+ }
+
+ NotifyCountPropertyChange();
+ NotifyIndexerPropertyChange();
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
+ ///
+ public bool Contains(UIElement item) => _backedList.Contains(item);
+
+ ///
+ public void CopyTo(UIElement[] array, int arrayIndex) => _backedList.CopyTo(array, arrayIndex);
+
+ ///
+ public int Count => _backedList.Count;
+
+ ///
+ public bool IsReadOnly => false;
+
+ ///
+ public int IndexOf(UIElement item) => _backedList.IndexOf(item);
+
+ ///
+ public bool Remove(UIElement item) => Remove(item, true);
+
+ ///
+ /// Removes the first occurrence of a specific object from the backed List.
+ ///
+ /// The object to remove from the backed List.
+ /// Whether to notify the changes or not.
+ ///
+ /// if was successfully removed from the backed List; otherwise, .
+ /// This method also returns if is not found from the backed List.
+ ///
+ public bool Remove(UIElement item, bool notifyChanges)
+ {
+ bool isSuccess = _backedList.Remove(item);
+
+ if (!isSuccess)
+ {
+ return false;
+ }
+
+ if (!notifyChanges)
+ {
+ return true;
+ }
+
+ NotifyCountPropertyChange();
+ NotifyIndexerPropertyChange();
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item));
+ return true;
+ }
+
+ ///
+ public void Insert(int index, UIElement item) => Insert(index, item, true);
+
+ ///
+ /// Inserts an item to the backed List at the specified index.
+ ///
+ /// The zero-based index at which should be inserted.
+ /// The object to insert into the backed List.
+ /// Whether to notify the changes or not.
+ public void Insert(int index, UIElement item, bool notifyChanges)
+ {
+ _backedList.Insert(index, item);
+
+ if (!notifyChanges)
+ {
+ return;
+ }
+
+ NotifyCountPropertyChange();
+ NotifyIndexerPropertyChange();
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
+ }
+
+ ///
+ public void RemoveAt(int index) => RemoveAt(index, true);
+
+ ///
+ /// Removes the backed List item at the specified index.
+ ///
+ /// The zero-based index of the item to remove.
+ /// Whether to notify the changes or not.
+ public void RemoveAt(int index, bool notifyChanges)
+ {
+ UIElement item = _backedList[index];
+ _backedList.Remove(item);
+
+ if (!notifyChanges)
+ {
+ return;
+ }
+
+ NotifyCountPropertyChange();
+ NotifyIndexerPropertyChange();
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
+ }
+
+ ///
+ [IndexerName("Item")]
+ public UIElement this[int index]
+ {
+ get => _backedList[index];
+ set
+ {
+ UIElement oldItem = _backedList[index];
+ UIElement newItem = _backedList[index] = value;
+
+ NotifyIndexerPropertyChange();
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, index));
+ }
+ }
+
+ private void NotifyCountPropertyChange() => PropertyChanged?.Invoke(this, CountPropertyChanged);
+ private void NotifyIndexerPropertyChange() => PropertyChanged?.Invoke(this, IndexerPropertyChanged);
+}
diff --git a/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Converters.cs b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Converters.cs
new file mode 100644
index 000000000..16c182960
--- /dev/null
+++ b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Converters.cs
@@ -0,0 +1,22 @@
+using Microsoft.UI.Xaml.Data;
+using System;
+
+namespace BackgroundTest.CustomControl.NewPipsPager;
+
+internal partial class IndexAddOneConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is int asInt)
+ {
+ return asInt + 1;
+ }
+
+ return value;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Enums.cs b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Enums.cs
new file mode 100644
index 000000000..4876940d7
--- /dev/null
+++ b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Enums.cs
@@ -0,0 +1,14 @@
+namespace BackgroundTest.CustomControl.NewPipsPager;
+
+public enum NewPipsPagerNavigationMode
+{
+ Auto,
+ Hidden,
+ Visible
+}
+
+public enum NewPipsPagerSelectionMode
+{
+ Click,
+ Hover
+}
\ No newline at end of file
diff --git a/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Events.cs b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Events.cs
new file mode 100644
index 000000000..614e4e201
--- /dev/null
+++ b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Events.cs
@@ -0,0 +1,379 @@
+using Microsoft.UI.Input;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+using System;
+using System.Linq;
+using System.Numerics;
+using Windows.Foundation;
+using Windows.System;
+
+namespace BackgroundTest.CustomControl.NewPipsPager;
+
+public partial class NewPipsPager
+{
+ #region Size Measure Override
+
+ protected override Size MeasureOverride(Size parentSize)
+ {
+ Vector2 containerSize = _pipsPagerItemsRepeater.ActualSize;
+ double containerTotalWidth = containerSize.X;
+ double containerTotalHeight = containerSize.Y;
+
+ GetNavigationButtonTotalSize(PreviousNavigationButtonMode,
+ _previousPageButton,
+ out double buttonPrevSizeTotalWidth,
+ out double buttonPrevSizeTotalHeight);
+
+ GetNavigationButtonTotalSize(NextNavigationButtonMode,
+ _nextPageButton,
+ out double buttonNextSizeTotalWidth,
+ out double buttonNextSizeTotalHeight);
+
+ Vector2 scrollViewportSize = parentSize.ToVector2();
+ double scrollViewportTotalWidth = scrollViewportSize.X;
+ double scrollViewportTotalHeight = scrollViewportSize.Y;
+
+ Size selfSize = base.MeasureOverride(parentSize);
+ double totalCalculatedWidth = selfSize.Width;
+ double totalCalculatedHeight = selfSize.Height;
+
+ if (Orientation == Orientation.Horizontal)
+ {
+ _pipsPagerScrollViewer.MaxWidth =
+ GetViewportSize(scrollViewportTotalWidth,
+ containerTotalWidth,
+ buttonPrevSizeTotalWidth,
+ buttonNextSizeTotalWidth,
+ _pipsButtonSize,
+ out bool isContainerLarger);
+
+ totalCalculatedWidth = 0;
+ totalCalculatedWidth += !isContainerLarger
+ ? containerTotalWidth
+ : _pipsPagerScrollViewer.MaxWidth;
+ totalCalculatedWidth += buttonPrevSizeTotalWidth +
+ buttonNextSizeTotalWidth;
+ }
+ else
+ {
+ _pipsPagerScrollViewer.MaxHeight =
+ GetViewportSize(scrollViewportTotalHeight,
+ containerTotalHeight,
+ buttonPrevSizeTotalHeight,
+ buttonNextSizeTotalHeight,
+ _pipsButtonSize,
+ out bool isContainerLarger);
+
+ totalCalculatedHeight = 0;
+ totalCalculatedHeight += !isContainerLarger
+ ? containerTotalHeight
+ : _pipsPagerScrollViewer.MaxHeight;
+ totalCalculatedHeight += buttonPrevSizeTotalHeight +
+ buttonNextSizeTotalHeight;
+ }
+
+ return new Size(totalCalculatedWidth, totalCalculatedHeight);
+ }
+
+ private static void GetNavigationButtonTotalSize(
+ NewPipsPagerNavigationMode mode,
+ Button element,
+ out double width,
+ out double height)
+ {
+ bool isVisible = mode != NewPipsPagerNavigationMode.Hidden;
+ Vector2 size = isVisible ? element.ActualSize : Vector2.Zero;
+ Thickness margin = isVisible ? element.Margin : default;
+ width = size.X + margin.Left + margin.Right;
+ height = size.Y + margin.Top + margin.Bottom;
+ }
+
+ private static double GetViewportSize(double initialViewportSize,
+ double initialContainerSize,
+ double previousSideElementSize,
+ double nextSideElementSize,
+ double perButtonSize,
+ out bool isContainerLarger)
+ {
+ // Decrease viewport based on total width of navigation buttons
+ initialViewportSize -= previousSideElementSize + nextSideElementSize;
+ isContainerLarger = initialContainerSize > initialViewportSize;
+
+ // Clamp to display only viewable pips
+ if (isContainerLarger)
+ {
+ double dividedPerButtonSize = Math.Floor(initialViewportSize / perButtonSize);
+ initialViewportSize = dividedPerButtonSize * perButtonSize;
+ int clampedNth = (int)dividedPerButtonSize;
+ if (clampedNth % 2 == 0)
+ {
+ initialViewportSize = (clampedNth - 1) * perButtonSize;
+ }
+
+ return Math.Max(initialViewportSize, 0);
+ }
+
+ return double.PositiveInfinity;
+ }
+
+ #endregion
+
+ #region UI Events - Navigation Buttons
+
+ private void PreviousPageButton_OnClick(object sender, RoutedEventArgs e) => ItemIndex--;
+
+ private void NextPageButton_OnClick(object sender, RoutedEventArgs e) => ItemIndex++;
+
+ private void KeyboardKeys_Pressed(object sender, KeyRoutedEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case VirtualKey.Left:
+ case VirtualKey.Up:
+ PreviousPageButton_OnClick(sender, e);
+ break;
+ case VirtualKey.Right:
+ case VirtualKey.Down:
+ NextPageButton_OnClick(sender, e);
+ break;
+ }
+ }
+
+ #endregion
+
+ #region UI Events - Orientation
+
+ private static void Orientation_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ NewPipsPager pager = (NewPipsPager)d;
+ Orientation orientation = (Orientation)e.NewValue;
+
+ Orientation_OnChange(pager, orientation);
+ }
+
+ private static void Orientation_OnChange(NewPipsPager pager, Orientation orientation)
+ {
+ string state = orientation == Orientation.Vertical
+ ? "VerticalOrientationView"
+ : "HorizontalOrientationView";
+ VisualStateManager.GoToState(pager, state, true);
+ }
+
+ #endregion
+
+ #region Property Changes
+
+ private static void ItemsCount_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not NewPipsPager pager)
+ {
+ return;
+ }
+
+ object obj = e.NewValue;
+ int value = 0;
+ if (obj is int asInt)
+ {
+ value = asInt;
+ }
+
+ pager.ItemsCount_OnChange(value);
+ }
+
+ private void ItemsCount_OnChange(int value)
+ {
+ if (!_isTemplateLoaded)
+ {
+ return;
+ }
+
+ if (value < 0)
+ {
+ throw new IndexOutOfRangeException("ItemsCount cannot be negative!");
+ }
+
+ int oldItemsCount = _itemsDummy.Length;
+
+ using (_atomicLock.EnterScope())
+ {
+ try
+ {
+
+ if (value == 0)
+ {
+ _itemsDummy = [];
+ return;
+ }
+
+ _itemsDummy = Enumerable.Range(0, value).ToArray();
+ }
+ finally
+ {
+ // Update ItemsSource if already assigned
+ _pipsPagerItemsRepeater.ItemsSource = _itemsDummy;
+ _pipsPagerItemsRepeater.UpdateLayout();
+
+ // Update index only if the count is invalid
+ int currentIndex = ItemIndex;
+ if (currentIndex > value ||
+ (value != 0 && currentIndex < 0))
+ {
+ ItemIndex = 0;
+ }
+
+ ItemsCountChanged?.Invoke(this, new ChangedStructItemArgs(oldItemsCount, value));
+ }
+ }
+ }
+
+ private static void ItemIndex_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ // Ignore if the old and new index are equal
+ if (e is not { NewValue: int asNewIndex, OldValue: int asOldIndex } ||
+ asNewIndex == asOldIndex)
+ {
+ return;
+ }
+
+ NewPipsPager pager = (NewPipsPager)d;
+
+ // Update navigation buttons state
+ UpdatePreviousButtonVisualState(pager);
+ UpdateNextButtonVisualState(pager);
+
+ // Update pip buttons state
+ UpdateAndBringSelectedPipToView(pager, asNewIndex, asOldIndex);
+ }
+
+ #endregion
+
+ #region ItemsRepeater
+
+ private void ItemsRepeater_ElementPrepared(ItemsRepeater sender, ItemsRepeaterElementPreparedEventArgs args)
+ {
+ if (args.Element is not Button asButton)
+ {
+ return;
+ }
+
+ if (asButton.Tag is not int asIndex)
+ {
+ return;
+ }
+
+ AssignPipButtonStyle(asButton,
+ asIndex != ItemIndex
+ ? PipButtonStyleNormal
+ : PipButtonStyleSelected);
+
+ // Avoid redundant loaded + unloaded events assignment
+ if (asButton.IsLoaded)
+ {
+ return;
+ }
+
+ asButton.Loaded += ItemsRepeaterPipButton_LoadedEvent;
+ asButton.Unloaded += ItemsRepeaterPipButton_UnloadedEvent;
+ }
+
+ private void ItemsRepeater_OnSizeChanged(object sender, SizeChangedEventArgs e) => InvalidateMeasure();
+
+ private void ItemsRepeaterPipButton_UnloadedEvent(object sender, RoutedEventArgs e)
+ {
+ Button button = (Button)sender;
+ if (SelectionMode == NewPipsPagerSelectionMode.Click)
+ {
+ button.Click -= ItemsRepeaterPipButton_OnClick;
+ }
+ else
+ {
+ button.PointerEntered -= ItemsRepeaterPipButton_OnClick;
+ }
+
+ button.Loaded -= ItemsRepeaterPipButton_LoadedEvent;
+ button.Unloaded -= ItemsRepeaterPipButton_UnloadedEvent;
+ }
+
+ private void ItemsRepeaterPipButton_LoadedEvent(object sender, RoutedEventArgs e)
+ {
+ Button button = (Button)sender;
+ if (SelectionMode == NewPipsPagerSelectionMode.Click)
+ {
+ button.Click += ItemsRepeaterPipButton_OnClick;
+ }
+ else
+ {
+ button.PointerEntered += ItemsRepeaterPipButton_OnClick;
+ }
+ }
+
+ private void ItemsRepeaterPipButton_OnClick(object sender, RoutedEventArgs args)
+ {
+ ItemIndex = (int)((Button)sender).Tag;
+ }
+
+ #endregion
+
+ #region ScrollViewer
+
+ private void ScrollViewer_OnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
+ {
+ if (!e.Pointer.IsInRange ||
+ sender is not UIElement element)
+ {
+ return;
+ }
+
+ PointerPoint pointer = e.GetCurrentPoint(element);
+ int orientation = pointer.Properties.MouseWheelDelta;
+ bool isHorizontal = Orientation == Orientation.Horizontal;
+ double delta = _pipsButtonSize * (orientation / 120d);
+
+ double toOffset = (isHorizontal
+ ? _pipsPagerScrollViewer.HorizontalOffset
+ : _pipsPagerScrollViewer.VerticalOffset) + -delta;
+ toOffset = Math.Round(toOffset / _pipsButtonSize) * _pipsButtonSize;
+
+ if (isHorizontal)
+ {
+ toOffset = Math.Clamp(toOffset, 0, _pipsPagerScrollViewer.ExtentWidth);
+ _pipsPagerScrollViewer.ChangeView(toOffset, _pipsPagerScrollViewer.VerticalOffset, _pipsPagerScrollViewer.ZoomFactor);
+ }
+ else
+ {
+ toOffset = Math.Clamp(toOffset, 0, _pipsPagerScrollViewer.ExtentHeight);
+ _pipsPagerScrollViewer.ChangeView(_pipsPagerScrollViewer.HorizontalOffset, toOffset, _pipsPagerScrollViewer.ZoomFactor);
+ }
+ }
+
+ #endregion
+
+ #region Loaded and Unloaded
+
+ private void NewPipsPager_Unloaded(object sender, RoutedEventArgs e)
+ {
+ UnapplyNavigationButtonEvents();
+ UnapplyKeyPressEvents();
+ UnapplyItemsRepeaterEvents();
+
+ _pipsPagerItemsRepeater.ItemsSource = null;
+ }
+
+ private void NewPipsPager_Loaded(object sender, RoutedEventArgs e)
+ {
+ ItemsCount_OnChange(ItemsCount);
+ UpdateAndBringSelectedPipToView(this, ItemIndex, -1);
+
+ if (sender is not NewPipsPager pager)
+ {
+ return;
+ }
+
+ // Update navigation buttons state
+ UpdatePreviousButtonVisualState(pager);
+ UpdateNextButtonVisualState(pager);
+ }
+
+ #endregion
+}
diff --git a/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Properties.cs b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Properties.cs
new file mode 100644
index 000000000..3ecd540ff
--- /dev/null
+++ b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Properties.cs
@@ -0,0 +1,264 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System.Threading;
+using Windows.Foundation;
+
+namespace BackgroundTest.CustomControl.NewPipsPager;
+
+public partial class NewPipsPager
+{
+ #region Events
+
+ public event TypedEventHandler>? ItemsCountChanged;
+ public event TypedEventHandler>? ItemIndexChanged;
+
+ #endregion
+
+ #region Properties
+
+ private static Style? PipButtonStyleNormal => field ??= TryGetStyle(PipButtonStyleNormalName);
+ private static Style? PipButtonStyleSelected => field ??= TryGetStyle(PipButtonStyleSelectedName);
+
+ public Orientation Orientation
+ {
+ get => (Orientation)GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ public int ItemsCount
+ {
+ get => (int)GetValue(ItemsCountProperty);
+ set => SetValue(ItemsCountProperty, value);
+ }
+
+ public int ItemIndex
+ {
+ get => (int)GetValue(ItemIndexProperty);
+ set
+ {
+ using (_atomicLock.EnterScope())
+ {
+ int itemsCount = ItemsCount;
+ if (itemsCount == 0)
+ {
+ return;
+ }
+
+ if (value < 0)
+ {
+ value = itemsCount - 1;
+ }
+
+ if (value >= itemsCount)
+ {
+ value = 0;
+ }
+
+ SetValue(ItemIndexProperty, value);
+ }
+ }
+ }
+
+ public NewPipsPagerSelectionMode SelectionMode
+ {
+ get => (NewPipsPagerSelectionMode)GetValue(SelectionModeProperty);
+ set => SetValue(SelectionModeProperty, value);
+ }
+
+ public NewPipsPagerNavigationMode PreviousNavigationButtonMode
+ {
+ get => (NewPipsPagerNavigationMode)GetValue(PreviousNavigationButtonModeProperty);
+ set => SetValue(PreviousNavigationButtonModeProperty, value);
+ }
+
+ public NewPipsPagerNavigationMode NextNavigationButtonMode
+ {
+ get => (NewPipsPagerNavigationMode)GetValue(NextNavigationButtonModeProperty);
+ set => SetValue(NextNavigationButtonModeProperty, value);
+ }
+
+ #endregion
+
+ #region Fields
+
+ private readonly Lock _atomicLock = new();
+
+ private int[] _itemsDummy = [];
+
+ #endregion
+
+ #region Dependency Change Methods
+
+ private static void UpdatePreviousButtonVisualState(NewPipsPager pager)
+ {
+ if (!pager._isTemplateLoaded)
+ {
+ return;
+ }
+
+ NewPipsPagerNavigationMode mode = pager.PreviousNavigationButtonMode;
+ if (mode == NewPipsPagerNavigationMode.Hidden)
+ {
+ VisualStateManager.GoToState(pager, NavButtonStatePreviousPageButtonCollapsed, true);
+ return;
+ }
+
+ pager._previousPageButton.IsEnabled = true;
+ VisualStateManager.GoToState(pager, NavButtonStatePreviousPageButtonVisible, true);
+ if (mode == NewPipsPagerNavigationMode.Visible)
+ {
+ return;
+ }
+
+ if (pager.ItemIndex > 0)
+ {
+ return;
+ }
+
+ pager._previousPageButton.IsEnabled = false;
+ VisualStateManager.GoToState(pager, NavButtonStatePreviousPageButtonHidden, true);
+ }
+
+ private static void UpdateNextButtonVisualState(NewPipsPager pager)
+ {
+ if (!pager._isTemplateLoaded)
+ {
+ return;
+ }
+
+ NewPipsPagerNavigationMode mode = pager.NextNavigationButtonMode;
+ if (mode == NewPipsPagerNavigationMode.Hidden)
+ {
+ VisualStateManager.GoToState(pager, NavButtonStateNextPageButtonCollapsed, true);
+ return;
+ }
+
+ pager._nextPageButton.IsEnabled = true;
+ VisualStateManager.GoToState(pager, NavButtonStateNextPageButtonVisible, true);
+ if (mode == NewPipsPagerNavigationMode.Visible)
+ {
+ return;
+ }
+
+ if (pager.ItemIndex + 1 < pager.ItemsCount)
+ {
+ return;
+ }
+
+ pager._nextPageButton.IsEnabled = false;
+ VisualStateManager.GoToState(pager, NavButtonStateNextPageButtonHidden, true);
+ }
+
+ private static void UpdateAndBringSelectedPipToView(NewPipsPager pager, int newIndex, int oldIndex)
+ {
+ if (pager.UpdateSelectedPipStyle(newIndex, oldIndex) is not { } asButton)
+ {
+ return;
+ }
+
+ BringIntoViewOptions options = new()
+ {
+ AnimationDesired = true
+ };
+ if (pager.Orientation == Orientation.Horizontal)
+ {
+ options.HorizontalAlignmentRatio = 0.5d;
+ }
+ else
+ {
+ options.VerticalAlignmentRatio = 0.5d;
+ }
+ asButton.StartBringIntoView(options);
+ }
+
+ private Button? UpdateSelectedPipStyle(int newIndex, int oldIndex)
+ {
+ if (!_isTemplateLoaded)
+ {
+ return null;
+ }
+
+ ItemsRepeater repeater = _pipsPagerItemsRepeater;
+
+ int childCount = repeater.ItemsSourceView.Count;
+ bool isUpdateNewChild = newIndex >= 0 && newIndex < childCount;
+ bool isUpdateOldChild = oldIndex >= 0 && oldIndex < childCount;
+
+ if (ItemsCount == 0)
+ {
+ return null;
+ }
+
+ Button? newIndexPipButton = repeater.GetOrCreateElement(newIndex) as Button;
+ Button? oldIndexPipButton = repeater.TryGetElement(oldIndex) as Button;
+
+ try
+ {
+ if (isUpdateOldChild)
+ {
+ AssignPipButtonStyle(oldIndexPipButton, PipButtonStyleNormal);
+ }
+
+ if (isUpdateNewChild)
+ {
+ return AssignPipButtonStyle(newIndexPipButton, PipButtonStyleSelected);
+ }
+
+ return newIndexPipButton;
+ }
+ finally
+ {
+ if (newIndex != oldIndex)
+ {
+ ItemIndexChanged?.Invoke(this, new ChangedStructItemArgs(oldIndex, newIndex));
+ }
+ }
+ }
+
+ private static Button? AssignPipButtonStyle(Button? button, Style? style)
+ {
+ if (button is null)
+ {
+ return button;
+ }
+
+ button.Style = style;
+ button.UpdateLayout();
+ VisualStateManager.GoToState(button, PipButtonStateNormal, true);
+ return button;
+ }
+
+ private static Style? TryGetStyle(string styleName)
+ {
+ if (Application.Current.Resources.TryGetValue(styleName, out object styleObj) &&
+ styleObj is Style asStyle)
+ {
+ return asStyle;
+ }
+
+ return null;
+ }
+
+ private static void PreviousNavigationButtonMode_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ UpdatePreviousButtonVisualState((NewPipsPager)d);
+ }
+
+ private static void NextNavigationButtonMode_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ UpdateNextButtonVisualState((NewPipsPager)d);
+ }
+
+ #endregion
+
+ #region Dependency Properties
+
+ public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(NewPipsPager), new PropertyMetadata(Orientation.Vertical, Orientation_OnChange));
+ public static readonly DependencyProperty ItemsCountProperty = DependencyProperty.Register(nameof(ItemsCount), typeof(int), typeof(NewPipsPager), new PropertyMetadata(0, ItemsCount_OnChange));
+ public static readonly DependencyProperty ItemIndexProperty = DependencyProperty.Register(nameof(ItemIndex), typeof(int), typeof(NewPipsPager), new PropertyMetadata(-1, ItemIndex_OnChange));
+ public static readonly DependencyProperty SelectionModeProperty = DependencyProperty.Register(nameof(SelectionMode), typeof(bool), typeof(NewPipsPager), new PropertyMetadata(NewPipsPagerSelectionMode.Click));
+ public static readonly DependencyProperty PreviousNavigationButtonModeProperty = DependencyProperty.Register(nameof(PreviousNavigationButtonMode), typeof(NewPipsPagerNavigationMode), typeof(NewPipsPager), new PropertyMetadata(NewPipsPagerNavigationMode.Auto, PreviousNavigationButtonMode_OnChange));
+ public static readonly DependencyProperty NextNavigationButtonModeProperty = DependencyProperty.Register(nameof(NextNavigationButtonMode), typeof(NewPipsPagerNavigationMode), typeof(NewPipsPager), new PropertyMetadata(NewPipsPagerNavigationMode.Auto, NextNavigationButtonMode_OnChange));
+
+ #endregion
+}
diff --git a/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Templates.cs b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Templates.cs
new file mode 100644
index 000000000..1d26d4135
--- /dev/null
+++ b/BackgroundTest/CustomControl/NewPipsPager/NewPipsPager.Templates.cs
@@ -0,0 +1,111 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System.Threading;
+
+namespace BackgroundTest.CustomControl.NewPipsPager;
+
+[TemplatePart(Name = TemplateNamePreviousPageButton, Type = typeof(Button))]
+[TemplatePart(Name = TemplateNameNextPageButton, Type = typeof(Button))]
+[TemplatePart(Name = TemplateNamePipsPagerScrollViewer, Type = typeof(ScrollViewer))]
+[TemplatePart(Name = TemplateNamePipsPagerItemsRepeater, Type = typeof(ItemsRepeater))]
+public partial class NewPipsPager
+{
+ #region Constants
+
+ private const string TemplateNamePreviousPageButton = "PreviousPageButton";
+ private const string TemplateNameNextPageButton = "NextPageButton";
+ private const string TemplateNamePipsPagerScrollViewer = "PipsPagerScrollViewer";
+ private const string TemplateNamePipsPagerItemsRepeater = "PipsPagerItemsRepeater";
+
+ private const string ResourceNamePipsPagerButtonSize = "PipsPagerButtonSize";
+
+ private const string PipButtonStateNormal = "Normal";
+ private const string PipButtonStyleNormalName = "NewPipsPagerButtonBaseStyle";
+ private const string PipButtonStyleSelectedName = "NewPipsPagerButtonBaseSelectedStyle";
+
+ private const string NavButtonStatePreviousPageButtonCollapsed = "PreviousPageButtonCollapsed";
+ private const string NavButtonStatePreviousPageButtonVisible = "PreviousPageButtonVisible";
+ private const string NavButtonStatePreviousPageButtonHidden = "PreviousPageButtonHidden";
+ private const string NavButtonStateNextPageButtonCollapsed = "NextPageButtonCollapsed";
+ private const string NavButtonStateNextPageButtonVisible = "NextPageButtonVisible";
+ private const string NavButtonStateNextPageButtonHidden = "NextPageButtonHidden";
+
+ #endregion
+
+ #region Fields
+
+ private Button _previousPageButton = null!;
+ private Button _nextPageButton = null!;
+ private ScrollViewer _pipsPagerScrollViewer = null!;
+ private ItemsRepeater _pipsPagerItemsRepeater = null!;
+
+ private double _pipsButtonSize;
+ private bool _isTemplateLoaded;
+
+ #endregion
+
+ #region Apply Template Methods
+
+ protected override void OnApplyTemplate()
+ {
+ _previousPageButton = this.GetTemplateChild