diff --git a/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs b/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs index b09c3287..5fc551d7 100644 --- a/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs +++ b/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs @@ -2,9 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if WINAPPSDK +#if WINUI3 using Microsoft.UI.Composition; -#else +#elif WINUI2 using Windows.UI.Composition; #endif diff --git a/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj b/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj index 196bf0ef..fde8d32e 100644 --- a/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj +++ b/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj @@ -10,7 +10,10 @@ </PropertyGroup> <ItemGroup> + <InternalsVisibleTo Include="CommunityToolkit.WinUI.Behaviors.Animations" /> + <InternalsVisibleTo Include="CommunityToolkit.WinUI.Media" /> <InternalsVisibleTo Include="CommunityToolkit.WinUI.Behaviors" /> + <PackageReference Include="PolySharp" Version="1.13.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs b/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs index 667f3e64..cc98af6e 100644 --- a/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs +++ b/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs @@ -86,11 +86,13 @@ public IList<IKeyFrame<TKeyFrame>> KeyFrames /// <summary> /// Gets the explicit target for the animation. This is the primary target property that is animated. /// </summary> - protected abstract string ExplicitTarget { get; } + protected abstract string? ExplicitTarget { get; } /// <inheritdoc/> public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) { + default(ArgumentNullException).ThrowIfNull(ExplicitTarget); + return builder.NormalizedKeyFrames<TKeyFrame, (Animation<TValue, TKeyFrame> This, EasingType? EasingTypeHint, EasingMode? EasingModeHint)>( property: ExplicitTarget, state: (this, easingTypeHint, easingModeHint), diff --git a/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs b/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs index ef1bef0b..dde31d2b 100644 --- a/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs +++ b/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs @@ -34,11 +34,13 @@ public abstract class CustomAnimation<TValue, TKeyFrame> : ImplicitAnimation<TVa #endif /// <inheritdoc/> - protected override string ExplicitTarget => Target!; + protected override string? ExplicitTarget => Target; /// <inheritdoc/> public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) { + default(ArgumentNullException).ThrowIfNull(ExplicitTarget); + return builder.NormalizedKeyFrames<TKeyFrame, (CustomAnimation<TValue, TKeyFrame> This, EasingType? EasingTypeHint, EasingMode? EasingModeHint)>( property: ExplicitTarget, state: (this, easingTypeHint, easingModeHint), diff --git a/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs b/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs index 2380e79c..f5c1fb77 100644 --- a/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs +++ b/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs @@ -46,6 +46,8 @@ protected ImplicitAnimation() /// <inheritdoc/> public CompositionAnimation GetAnimation(UIElement element, out string? target) { + default(ArgumentNullException).ThrowIfNull(ExplicitTarget); + NormalizedKeyFrameAnimationBuilder<TKeyFrame>.Composition builder = new( ExplicitTarget, Delay ?? DefaultDelay, diff --git a/components/Animations/src/Xaml/Default/ClipAnimation.cs b/components/Animations/src/Xaml/Default/ClipAnimation.cs index 69cb97a7..9f8330ea 100644 --- a/components/Animations/src/Xaml/Default/ClipAnimation.cs +++ b/components/Animations/src/Xaml/Default/ClipAnimation.cs @@ -12,9 +12,7 @@ namespace CommunityToolkit.WinUI.Animations; public sealed class ClipAnimation : Animation<Thickness?, Thickness> { /// <inheritdoc/> -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations - protected override string ExplicitTarget => throw new NotImplementedException(); -#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations + protected override string? ExplicitTarget => null; /// <inheritdoc/> public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) diff --git a/components/Extensions/src/ArgumentNullExceptionExtensions.cs b/components/Extensions/src/ArgumentNullExceptionExtensions.cs new file mode 100644 index 00000000..545654bf --- /dev/null +++ b/components/Extensions/src/ArgumentNullExceptionExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System; + +/// <summary> +/// Throw helper extensions for <see cref="ArgumentNullException"/>. +/// </summary> +internal static class ArgumentNullExceptionExtensions +{ + /// <summary> + /// Throws an <see cref="ArgumentNullException"/> for a given parameter name. + /// </summary> + /// <param name="_">Dummy value to invoke the extension upon (always pass <see langword="null"/>.</param> + /// <param name="parameterName">The name of the parameter to report in the exception.</param> + /// <exception cref="ArgumentNullException">Thrown with <paramref name="parameterName"/>.</exception> + [DoesNotReturn] + public static void Throw(this ArgumentNullException? _, string? parameterName) + { + throw new ArgumentNullException(parameterName); + } + + /// <summary> + /// Throws an <see cref="ArgumentNullException"/> if <paramref name="argument"/> is <see langword="null"/>. + /// </summary> + /// <param name="_">Dummy value to invoke the extension upon (always pass <see langword="null"/>.</param> + /// <param name="argument">The reference type argument to validate as non-<see langword="null"/>.</param> + /// <param name="parameterName">The name of the parameter with which <paramref name="argument"/> corresponds.</param> + /// <exception cref="ArgumentNullException">Thrown if <paramref name="argument"/> is <see langword="null"/>.</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfNull(this ArgumentNullException? _, [NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? parameterName = null) + { + if (argument is null) + { + Throw(parameterName); + } + } + + /// <summary> + /// Throws an <see cref="ArgumentNullException"/> if <paramref name="argument"/> is <see langword="null"/>. + /// </summary> + /// <param name="_">Dummy value to invoke the extension upon (always pass <see langword="null"/>.</param> + /// <param name="argument">The pointer argument to validate as non-<see langword="null"/>.</param> + /// <param name="parameterName">The name of the parameter with which <paramref name="argument"/> corresponds.</param> + /// <exception cref="ArgumentNullException">Thrown if <paramref name="argument"/> is <see langword="null"/>.</exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void ThrowIfNull(this ArgumentNullException? _, [NotNull] void* argument, [CallerArgumentExpression(nameof(argument))] string? parameterName = null) + { + if (argument is null) + { + Throw(parameterName); + } + } + + /// <inheritdoc cref="Throw(ArgumentNullException?, string?)"/> + [DoesNotReturn] + private static void Throw(string? parameterName) + { + throw new ArgumentNullException(parameterName); + } +} diff --git a/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj b/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj index dceba6eb..e4df1fff 100644 --- a/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj +++ b/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj @@ -6,11 +6,21 @@ <!-- Rns suffix is required for namespaces shared across projects. See https://github.com/CommunityToolkit/Labs-Windows/issues/152 --> <RootNamespace>CommunityToolkit.WinUI.ExtensionsRns</RootNamespace> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> <!-- Sets this up as a toolkit component's source project --> <Import Project="$(ToolingDirectory)\ToolkitComponent.SourceProject.props" /> + <ItemGroup> <PackageReference Include="CommunityToolkit.Common" Version="8.1.0" /> + + <PackageReference Include="PolySharp" Version="1.13.1"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + + <InternalsVisibleTo Include="CommunityToolkit.WinUI.Animations" /> + <InternalsVisibleTo Include="CommunityToolkit.WinUI.Media" /> </ItemGroup> </Project> diff --git a/components/Extensions/src/Markup/FontIconExtension.cs b/components/Extensions/src/Markup/FontIconExtension.cs index 4afac400..4a090833 100644 --- a/components/Extensions/src/Markup/FontIconExtension.cs +++ b/components/Extensions/src/Markup/FontIconExtension.cs @@ -23,6 +23,8 @@ public class FontIconExtension : TextIconExtension /// <inheritdoc/> protected override object ProvideValue() { + default(ArgumentNullException).ThrowIfNull(Glyph); + var fontIcon = new FontIcon { Glyph = Glyph, diff --git a/components/Media/OpenSolution.bat b/components/Media/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Media/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Media/samples/Assets/icon.png b/components/Media/samples/Assets/icon.png new file mode 100644 index 00000000..5f574cec Binary files /dev/null and b/components/Media/samples/Assets/icon.png differ diff --git a/components/Media/samples/Dependencies.props b/components/Media/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Media/samples/Dependencies.props @@ -0,0 +1,31 @@ +<!-- + WinUI 2 under UWP uses TargetFramework uap10.0.* + WinUI 3 under WinAppSdk uses TargetFramework net6.0-windows10.* + However, under Uno-powered platforms, both WinUI 2 and 3 can share the same TargetFramework. + + MSBuild doesn't play nicely with this out of the box, so we've made it easy for you. + + For .NET Standard packages, you can use the Nuget Package Manager in Visual Studio. + For UWP / WinAppSDK / Uno packages, place the package references here. +--> +<Project> + <!-- WinUI 2 / UWP --> + <ItemGroup Condition="'$(IsUwp)' == 'true'"> + <!-- <PackageReference Include="Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.2"/> --> + </ItemGroup> + + <!-- WinUI 2 / Uno --> + <ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '2'"> + <!-- <PackageReference Include="Uno.Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.11"/> --> + </ItemGroup> + + <!-- WinUI 3 / WinAppSdk --> + <ItemGroup Condition="'$(IsWinAppSdk)' == 'true'"> + <!-- <PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.2"/> --> + </ItemGroup> + + <!-- WinUI 3 / Uno --> + <ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '3'"> + <!-- <PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.100-dev.15.g12261e2626"/> --> + </ItemGroup> +</Project> diff --git a/components/Media/samples/Media.Samples.csproj b/components/Media/samples/Media.Samples.csproj new file mode 100644 index 00000000..ce5843af --- /dev/null +++ b/components/Media/samples/Media.Samples.csproj @@ -0,0 +1,8 @@ +<Project Sdk="MSBuild.Sdk.Extras/3.0.23"> + <PropertyGroup> + <ToolkitComponentName>Media</ToolkitComponentName> + </PropertyGroup> + + <!-- Sets this up as a toolkit component's sample project --> + <Import Project="$(ToolingDirectory)\ToolkitComponent.SampleProject.props" /> +</Project> diff --git a/components/Media/src/AdditionalAssemblyInfo.cs b/components/Media/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..7e58d70e --- /dev/null +++ b/components/Media/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("Media.Tests.Uwp")] +[assembly: InternalsVisibleTo("Media.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Media/src/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs b/components/Media/src/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs new file mode 100644 index 00000000..4663e338 --- /dev/null +++ b/components/Media/src/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; +using static CommunityToolkit.WinUI.Animations.AnimationExtensions; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// A custom animation targeting a property on an <see cref="IPipelineEffect"/> instance. +/// </summary> +/// <typeparam name="TEffect">The type of effect to animate.</typeparam> +/// <typeparam name="TValue"> +/// The type to use for the public <see cref="Animation{TValue,TKeyFrame}.To"/> and <see cref="Animation{TValue,TKeyFrame}.From"/> +/// properties. This can differ from <typeparamref name="TKeyFrame"/> to facilitate XAML parsing. +/// </typeparam> +/// <typeparam name="TKeyFrame">The actual type of keyframe values in use.</typeparam> +public abstract class EffectAnimation<TEffect, TValue, TKeyFrame> : Animation<TValue, TKeyFrame> + where TEffect : class, IPipelineEffect + where TKeyFrame : unmanaged +{ + /// <summary> + /// Gets or sets the linked <typeparamref name="TEffect"/> instance to animate. + /// </summary> + public TEffect? Target + { + get => (TEffect?)GetValue(TargetProperty); + set => SetValue(TargetProperty, value); + } + + /// <summary> + /// Identifies the <seealso cref="Target"/> dependency property. + /// </summary> + public static readonly DependencyProperty TargetProperty = DependencyProperty.Register( + nameof(Target), + typeof(TEffect), + typeof(EffectAnimation<TEffect, TValue, TKeyFrame>), + new PropertyMetadata(null)); + + /// <inheritdoc/> + public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) + { + if (Target is not TEffect target) + { + static AnimationBuilder ThrowArgumentNullException() => throw new ArgumentNullException("The target effect is null, make sure to set the Target property"); + + return ThrowArgumentNullException(); + } + + if (ExplicitTarget is not string explicitTarget) + { + static AnimationBuilder ThrowArgumentNullException() + { + throw new ArgumentNullException( + "The target effect cannot be animated at this time. If you're targeting one of the " + + "built-in effects, make sure that the PipelineEffect.IsAnimatable property is set to true."); + } + + return ThrowArgumentNullException(); + } + + NormalizedKeyFrameAnimationBuilder<TKeyFrame>.Composition keyFrameBuilder = new( + explicitTarget, + Delay ?? delayHint ?? DefaultDelay, + Duration ?? durationHint ?? DefaultDuration, + Repeat, + DelayBehavior); + + AppendToBuilder(keyFrameBuilder, easingTypeHint, easingModeHint); + + CompositionAnimation animation = keyFrameBuilder.GetAnimation(target.Brush!, out _); + + return builder.ExternalAnimation(target.Brush!, animation); + } +} diff --git a/components/Media/src/Animations/BlurEffectAnimation.cs b/components/Media/src/Animations/BlurEffectAnimation.cs new file mode 100644 index 00000000..dd44d3c5 --- /dev/null +++ b/components/Media/src/Animations/BlurEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="BlurEffect.Amount"/>. +/// </summary> +public sealed class BlurEffectAnimation : EffectAnimation<BlurEffect, double?, double> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/ColorEffectAnimation.cs b/components/Media/src/Animations/ColorEffectAnimation.cs new file mode 100644 index 00000000..c3aaffd6 --- /dev/null +++ b/components/Media/src/Animations/ColorEffectAnimation.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="TintEffect.Color"/>. +/// </summary> +public sealed class ColorEffectAnimation : EffectAnimation<TintEffect, Color?, Color> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (Color?, Color?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/CrossFadeEffectAnimation.cs b/components/Media/src/Animations/CrossFadeEffectAnimation.cs new file mode 100644 index 00000000..1ee76d92 --- /dev/null +++ b/components/Media/src/Animations/CrossFadeEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="CrossFadeEffect.Factor"/>. +/// </summary> +public sealed class CrossFadeEffectAnimation : EffectAnimation<CrossFadeEffect, double?, double> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/ExposureEffectAnimation.cs b/components/Media/src/Animations/ExposureEffectAnimation.cs new file mode 100644 index 00000000..7bd45706 --- /dev/null +++ b/components/Media/src/Animations/ExposureEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="ExposureEffect.Amount"/>. +/// </summary> +public sealed class ExposureEffectAnimation : EffectAnimation<ExposureEffect, double?, double> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/HueRotationEffectAnimation.cs b/components/Media/src/Animations/HueRotationEffectAnimation.cs new file mode 100644 index 00000000..723806c1 --- /dev/null +++ b/components/Media/src/Animations/HueRotationEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="HueRotationEffect.Angle"/>. +/// </summary> +public sealed class HueRotationEffectAnimation : EffectAnimation<HueRotationEffect, double?, double> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/OpacityEffectAnimation.cs b/components/Media/src/Animations/OpacityEffectAnimation.cs new file mode 100644 index 00000000..e4f76b5b --- /dev/null +++ b/components/Media/src/Animations/OpacityEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="OpacityEffect.Value"/>. +/// </summary> +public sealed class OpacityEffectAnimation : EffectAnimation<OpacityEffect, double?, double> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/SaturationEffectAnimation.cs b/components/Media/src/Animations/SaturationEffectAnimation.cs new file mode 100644 index 00000000..014a8648 --- /dev/null +++ b/components/Media/src/Animations/SaturationEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="SaturationEffect.Value"/>. +/// </summary> +public sealed class SaturationEffectAnimation : EffectAnimation<SaturationEffect, double?, double> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/SepiaEffectAnimation.cs b/components/Media/src/Animations/SepiaEffectAnimation.cs new file mode 100644 index 00000000..f32d62e3 --- /dev/null +++ b/components/Media/src/Animations/SepiaEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// <summary> +/// An effect animation that targets <see cref="SepiaEffect.Intensity"/>. +/// </summary> +public sealed class SepiaEffectAnimation : EffectAnimation<SepiaEffect, double?, double> +{ + /// <inheritdoc/> + protected override string? ExplicitTarget => Target?.Id; + + /// <inheritdoc/> + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Brushes/AcrylicBrush.cs b/components/Media/src/Brushes/AcrylicBrush.cs new file mode 100644 index 00000000..ba95daea --- /dev/null +++ b/components/Media/src/Brushes/AcrylicBrush.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINUI2 +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; +using Windows.UI.Composition; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A <see cref="XamlCompositionBrush"/> that implements an acrylic effect with customizable parameters +/// </summary> +public sealed class AcrylicBrush : XamlCompositionEffectBrushBase +{ + /// <summary> + /// The <see cref="EffectSetter{T}"/> instance in use to set the blur amount + /// </summary> + /// <remarks>This is only set when <see cref="BackgroundSource"/> is <see cref="AcrylicBackgroundSource.Backdrop"/></remarks> + private EffectSetter<float>? blurAmountSetter; + + /// <summary> + /// The <see cref="EffectSetter{T}"/> instance in use to set the tint color + /// </summary> + private EffectSetter<Color>? tintColorSetter; + + /// <summary> + /// The <see cref="EffectSetter{T}"/> instance in use to set the tint mix amount + /// </summary> + private EffectSetter<float>? tintOpacitySetter; + + /// <summary> + /// Gets or sets the background source mode for the effect (the default is <see cref="AcrylicBackgroundSource.Backdrop"/>). + /// </summary> + public AcrylicBackgroundSource BackgroundSource + { + get => (AcrylicBackgroundSource)GetValue(BackgroundSourceProperty); + set => SetValue(BackgroundSourceProperty, value); + } + + /// <summary> + /// Identifies the <see cref="BackgroundSource"/> dependency property. + /// </summary> + public static readonly DependencyProperty BackgroundSourceProperty = DependencyProperty.Register( + nameof(BackgroundSource), + typeof(AcrylicBackgroundSource), + typeof(AcrylicBrush), + new PropertyMetadata(AcrylicBackgroundSource.Backdrop, OnSourcePropertyChanged)); + + /// <summary> + /// Updates the UI when <see cref="BackgroundSource"/> changes + /// </summary> + /// <param name="d">The current <see cref="AcrylicBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="BackgroundSourceProperty"/></param> + private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush != null) + { + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + /// <summary> + /// Gets or sets the blur amount for the effect (must be a positive value) + /// </summary> + /// <remarks>This property is ignored when the active mode is <see cref="AcrylicBackgroundSource.HostBackdrop"/></remarks> + public double BlurAmount + { + get => (double)GetValue(BlurAmountProperty); + set => SetValue(BlurAmountProperty, Math.Max(value, 0)); + } + + /// <summary> + /// Identifies the <see cref="BlurAmount"/> dependency property. + /// </summary> + public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register( + nameof(BlurAmount), + typeof(double), + typeof(AcrylicBrush), + new PropertyMetadata(0.0, OnBlurAmountPropertyChanged)); + + /// <summary> + /// Updates the UI when <see cref="BackgroundSource"/> changes + /// </summary> + /// <param name="d">The current <see cref="AcrylicBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="BackgroundSourceProperty"/></param> + private static void OnBlurAmountPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.BackgroundSource != AcrylicBackgroundSource.HostBackdrop && // Blur is fixed by OS when using HostBackdrop source. + brush.CompositionBrush is CompositionBrush target) + { + brush.blurAmountSetter?.Invoke(target, (float)(double)e.NewValue); + } + } + + /// <summary> + /// Gets or sets the tint for the effect + /// </summary> + public Color TintColor + { + get => (Color)GetValue(TintColorProperty); + set => SetValue(TintColorProperty, value); + } + + /// <summary> + /// Identifies the <see cref="TintColor"/> dependency property. + /// </summary> + public static readonly DependencyProperty TintColorProperty = DependencyProperty.Register( + nameof(TintColor), + typeof(Color), + typeof(AcrylicBrush), + new PropertyMetadata(default(Color), OnTintColorPropertyChanged)); + + /// <summary> + /// Updates the UI when <see cref="TintColor"/> changes + /// </summary> + /// <param name="d">The current <see cref="AcrylicBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="TintColorProperty"/></param> + private static void OnTintColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush is CompositionBrush target) + { + brush.tintColorSetter?.Invoke(target, (Color)e.NewValue); + } + } + + /// <summary> + /// Gets or sets the tint opacity factor for the effect (default is 0.5, must be in the [0, 1] range) + /// </summary> + public double TintOpacity + { + get => (double)GetValue(TintOpacityProperty); + set => SetValue(TintOpacityProperty, Math.Clamp(value, 0, 1)); + } + + /// <summary> + /// Identifies the <see cref="TintOpacity"/> dependency property. + /// </summary> + public static readonly DependencyProperty TintOpacityProperty = DependencyProperty.Register( + nameof(TintOpacity), + typeof(double), + typeof(AcrylicBrush), + new PropertyMetadata(0.5, OnTintOpacityPropertyChanged)); + + /// <summary> + /// Updates the UI when <see cref="TintOpacity"/> changes + /// </summary> + /// <param name="d">The current <see cref="AcrylicBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="TintOpacityProperty"/></param> + private static void OnTintOpacityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush is CompositionBrush target) + { + brush.tintOpacitySetter?.Invoke(target, (float)(double)e.NewValue); + } + } + + /// <summary> + /// Gets or sets the <see cref="Uri"/> for the texture to use + /// </summary> + public Uri TextureUri + { + get => (Uri)GetValue(TextureUriProperty); + set => SetValue(TextureUriProperty, value); + } + + /// <summary> + /// Identifies the <see cref="TextureUri"/> dependency property. + /// </summary> + public static readonly DependencyProperty TextureUriProperty = DependencyProperty.Register( + nameof(TextureUri), + typeof(Uri), + typeof(AcrylicBrush), + new PropertyMetadata(default, OnTextureUriPropertyChanged)); + + /// <summary> + /// Updates the UI when <see cref="TextureUri"/> changes + /// </summary> + /// <param name="d">The current <see cref="AcrylicBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="TextureUriProperty"/></param> + private static void OnTextureUriPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush != null) + { + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + switch (BackgroundSource) + { + case AcrylicBackgroundSource.Backdrop: + return PipelineBuilder.FromBackdropAcrylic( + TintColor, + out this.tintColorSetter, + (float)TintOpacity, + out this.tintOpacitySetter, + (float)BlurAmount, + out blurAmountSetter, + TextureUri); + case AcrylicBackgroundSource.HostBackdrop: + return PipelineBuilder.FromHostBackdropAcrylic( + TintColor, + out this.tintColorSetter, + (float)TintOpacity, + out this.tintOpacitySetter, + TextureUri); + default: throw new ArgumentOutOfRangeException(nameof(BackgroundSource), $"Invalid acrylic source: {BackgroundSource}"); + } + } +} +#endif diff --git a/components/Media/src/Brushes/BackdropBlurBrush.cs b/components/Media/src/Brushes/BackdropBlurBrush.cs new file mode 100644 index 00000000..b6159604 --- /dev/null +++ b/components/Media/src/Brushes/BackdropBlurBrush.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Example brush from https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.xamlcompositionbrushbase + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// The <see cref="BackdropBlurBrush"/> is a <see cref="Brush"/> that blurs whatever is behind it in the application. +/// </summary> +public class BackdropBlurBrush : XamlCompositionEffectBrushBase +{ + /// <summary> + /// The <see cref="EffectSetter{T}"/> instance currently in use + /// </summary> + private EffectSetter<float>? amountSetter; + + /// <summary> + /// Gets or sets the amount of gaussian blur to apply to the background. + /// </summary> + public double Amount + { + get => (double)GetValue(AmountProperty); + set => SetValue(AmountProperty, value); + } + + /// <summary> + /// Identifies the <see cref="Amount"/> dependency property. + /// </summary> + public static readonly DependencyProperty AmountProperty = DependencyProperty.Register( + nameof(Amount), + typeof(double), + typeof(BackdropBlurBrush), + new PropertyMetadata(0.0, new PropertyChangedCallback(OnAmountChanged))); + + /// <summary> + /// Updates the UI when <see cref="Amount"/> changes + /// </summary> + /// <param name="d">The current <see cref="BackdropBlurBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="AmountProperty"/></param> + private static void OnAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BackdropBlurBrush brush && + brush.CompositionBrush is CompositionBrush target) + { + brush.amountSetter?.Invoke(target, (float)brush.Amount); + } + } + + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Blur((float)Amount, out this.amountSetter); + } +} diff --git a/components/Media/src/Brushes/BackdropGammaTransferBrush.cs b/components/Media/src/Brushes/BackdropGammaTransferBrush.cs new file mode 100644 index 00000000..1b7de60b --- /dev/null +++ b/components/Media/src/Brushes/BackdropGammaTransferBrush.cs @@ -0,0 +1,406 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Graphics.Canvas.Effects; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A brush which alters the colors of whatever is behind it in the application by applying a per-channel gamma transfer function. See https://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_GammaTransferEffect.htm. +/// </summary> +public class BackdropGammaTransferBrush : XamlCompositionBrushBase +{ + /// <summary> + /// Gets or sets the amount of scale to apply to the alpha chennel. + /// </summary> + public double AlphaAmplitude + { + get => (double)GetValue(AlphaAmplitudeProperty); + set => SetValue(AlphaAmplitudeProperty, value); + } + + /// <summary> + /// Identifies the <see cref="AlphaAmplitude"/> dependency property. + /// </summary> + public static readonly DependencyProperty AlphaAmplitudeProperty = DependencyProperty.Register( + nameof(AlphaAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(AlphaAmplitude)))); + + /// <summary> + /// Gets or sets a value indicating whether to disable alpha transfer. + /// </summary> + public bool AlphaDisable + { + get => (bool)GetValue(AlphaDisableProperty); + set => SetValue(AlphaDisableProperty, value); + } + + /// <summary> + /// Identifies the <see cref="AlphaDisable"/> dependency property. + /// </summary> + public static readonly DependencyProperty AlphaDisableProperty = DependencyProperty.Register( + nameof(AlphaDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(AlphaDisable)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the alpha chennel. + /// </summary> + public double AlphaExponent + { + get => (double)GetValue(AlphaExponentProperty); + set => SetValue(AlphaExponentProperty, value); + } + + /// <summary> + /// Identifies the <see cref="AlphaExponent"/> dependency property. + /// </summary> + public static readonly DependencyProperty AlphaExponentProperty = DependencyProperty.Register( + nameof(AlphaExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(AlphaExponent)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the alpha chennel. + /// </summary> + public double AlphaOffset + { + get => (double)GetValue(AlphaOffsetProperty); + set => SetValue(AlphaOffsetProperty, value); + } + + /// <summary> + /// Identifies the <see cref="AlphaOffset"/> dependency property. + /// </summary> + public static readonly DependencyProperty AlphaOffsetProperty = DependencyProperty.Register( + nameof(AlphaOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(AlphaOffset)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Blue chennel. + /// </summary> + public double BlueAmplitude + { + get => (double)GetValue(BlueAmplitudeProperty); + set => SetValue(BlueAmplitudeProperty, value); + } + + /// <summary> + /// Identifies the <see cref="BlueAmplitude"/> dependency property. + /// </summary> + public static readonly DependencyProperty BlueAmplitudeProperty = DependencyProperty.Register( + nameof(BlueAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(BlueAmplitude)))); + + /// <summary> + /// Gets or sets a value indicating whether to disable Blue transfer. + /// </summary> + public bool BlueDisable + { + get => (bool)GetValue(BlueDisableProperty); + set => SetValue(BlueDisableProperty, value); + } + + /// <summary> + /// Identifies the <see cref="BlueDisable"/> dependency property. + /// </summary> + public static readonly DependencyProperty BlueDisableProperty = DependencyProperty.Register( + nameof(BlueDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(BlueDisable)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Blue chennel. + /// </summary> + public double BlueExponent + { + get => (double)GetValue(BlueExponentProperty); + set => SetValue(BlueExponentProperty, value); + } + + /// <summary> + /// Identifies the <see cref="BlueExponent"/> dependency property. + /// </summary> + public static readonly DependencyProperty BlueExponentProperty = DependencyProperty.Register( + nameof(BlueExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(BlueExponent)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Blue chennel. + /// </summary> + public double BlueOffset + { + get => (double)GetValue(BlueOffsetProperty); + set => SetValue(BlueOffsetProperty, value); + } + + /// <summary> + /// Identifies the <see cref="BlueOffset"/> dependency property. + /// </summary> + public static readonly DependencyProperty BlueOffsetProperty = DependencyProperty.Register( + nameof(BlueOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(BlueOffset)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Green chennel. + /// </summary> + public double GreenAmplitude + { + get => (double)GetValue(GreenAmplitudeProperty); + set => SetValue(GreenAmplitudeProperty, value); + } + + /// <summary> + /// Identifies the <see cref="GreenAmplitude"/> dependency property. + /// </summary> + public static readonly DependencyProperty GreenAmplitudeProperty = DependencyProperty.Register( + nameof(GreenAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(GreenAmplitude)))); + + /// <summary> + /// Gets or sets a value indicating whether to disable Green transfer. + /// </summary> + public bool GreenDisable + { + get => (bool)GetValue(GreenDisableProperty); + set => SetValue(GreenDisableProperty, value); + } + + /// <summary> + /// Identifies the <see cref="GreenDisable"/> dependency property. + /// </summary> + public static readonly DependencyProperty GreenDisableProperty = DependencyProperty.Register( + nameof(GreenDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(GreenDisable)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Green chennel. + /// </summary> + public double GreenExponent + { + get => (double)GetValue(GreenExponentProperty); + set => SetValue(GreenExponentProperty, value); + } + + /// <summary> + /// Identifies the <see cref="GreenExponent"/> dependency property. + /// </summary> + public static readonly DependencyProperty GreenExponentProperty = DependencyProperty.Register( + nameof(GreenExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(GreenExponent)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Green chennel. + /// </summary> + public double GreenOffset + { + get => (double)GetValue(GreenOffsetProperty); + set => SetValue(GreenOffsetProperty, value); + } + + /// <summary> + /// Identifies the <see cref="GreenOffset"/> dependency property. + /// </summary> + public static readonly DependencyProperty GreenOffsetProperty = DependencyProperty.Register( + nameof(GreenOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(GreenOffset)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Red chennel. + /// </summary> + public double RedAmplitude + { + get => (double)GetValue(RedAmplitudeProperty); + set => SetValue(RedAmplitudeProperty, value); + } + + /// <summary> + /// Identifies the <see cref="RedAmplitude"/> dependency property. + /// </summary> + public static readonly DependencyProperty RedAmplitudeProperty = DependencyProperty.Register( + nameof(RedAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(RedAmplitude)))); + + /// <summary> + /// Gets or sets a value indicating whether to disable Red transfer. + /// </summary> + public bool RedDisable + { + get => (bool)GetValue(RedDisableProperty); + set => SetValue(RedDisableProperty, value); + } + + /// <summary> + /// Identifies the <see cref="RedDisable"/> dependency property. + /// </summary> + public static readonly DependencyProperty RedDisableProperty = DependencyProperty.Register( + nameof(RedDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(RedDisable)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Red chennel. + /// </summary> + public double RedExponent + { + get => (double)GetValue(RedExponentProperty); + set => SetValue(RedExponentProperty, value); + } + + /// <summary> + /// Identifies the <see cref="RedExponent"/> dependency property. + /// </summary> + public static readonly DependencyProperty RedExponentProperty = DependencyProperty.Register( + nameof(RedExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(RedExponent)))); + + /// <summary> + /// Gets or sets the amount of scale to apply to the Red chennel. + /// </summary> + public double RedOffset + { + get => (double)GetValue(RedOffsetProperty); + set => SetValue(RedOffsetProperty, value); + } + + /// <summary> + /// Identifies the <see cref="RedOffset"/> dependency property. + /// </summary> + public static readonly DependencyProperty RedOffsetProperty = DependencyProperty.Register( + nameof(RedOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(RedOffset)))); + + private static PropertyChangedCallback OnScalarPropertyChangedHelper(string propertyname) + { + return (d, e) => + { + var brush = (BackdropGammaTransferBrush)d; + + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("GammaTransfer." + propertyname, (float)(double)e.NewValue); + }; + } + + private static PropertyChangedCallback OnBooleanPropertyChangedHelper(string propertyname) + { + return (d, e) => + { + var brush = (BackdropGammaTransferBrush)d; + + // We can't animate our boolean properties so recreate our internal brush. + brush.OnDisconnected(); + brush.OnConnected(); + }; + } + + /// <inheritdoc/> + protected override void OnConnected() + { + // Delay creating composition resources until they're required. + if (CompositionBrush == null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + var backdrop = Window.Current.Compositor.CreateBackdropBrush(); + + // Use a Win2D blur affect applied to a CompositionBackdropBrush. + var graphicsEffect = new GammaTransferEffect + { + Name = "GammaTransfer", + AlphaAmplitude = (float)AlphaAmplitude, + AlphaDisable = AlphaDisable, + AlphaExponent = (float)AlphaExponent, + AlphaOffset = (float)AlphaOffset, + RedAmplitude = (float)RedAmplitude, + RedDisable = RedDisable, + RedExponent = (float)RedExponent, + RedOffset = (float)RedOffset, + GreenAmplitude = (float)GreenAmplitude, + GreenDisable = GreenDisable, + GreenExponent = (float)GreenExponent, + GreenOffset = (float)GreenOffset, + BlueAmplitude = (float)BlueAmplitude, + BlueDisable = BlueDisable, + BlueExponent = (float)BlueExponent, + BlueOffset = (float)BlueOffset, + Source = new CompositionEffectSourceParameter("backdrop") + }; + + var effectFactory = Window.Current.Compositor.CreateEffectFactory(graphicsEffect, new[] + { + "GammaTransfer.AlphaAmplitude", + "GammaTransfer.AlphaExponent", + "GammaTransfer.AlphaOffset", + "GammaTransfer.RedAmplitude", + "GammaTransfer.RedExponent", + "GammaTransfer.RedOffset", + "GammaTransfer.GreenAmplitude", + "GammaTransfer.GreenExponent", + "GammaTransfer.GreenOffset", + "GammaTransfer.BlueAmplitude", + "GammaTransfer.BlueExponent", + "GammaTransfer.BlueOffset", + }); + var effectBrush = effectFactory.CreateBrush(); + + effectBrush.SetSourceParameter("backdrop", backdrop); + + CompositionBrush = effectBrush; + } + } + + /// <inheritdoc/> + protected override void OnDisconnected() + { + // Dispose of composition resources when no longer in use. + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + } +} diff --git a/components/Media/src/Brushes/BackdropInvertBrush.cs b/components/Media/src/Brushes/BackdropInvertBrush.cs new file mode 100644 index 00000000..b5cc729e --- /dev/null +++ b/components/Media/src/Brushes/BackdropInvertBrush.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Example brush from https://blogs.windows.com/buildingapps/2017/07/18/working-brushes-content-xaml-visual-layer-interop-part-one/#z70vPv1QMAvZsceo.97 + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// The <see cref="BackdropInvertBrush"/> is a <see cref="Brush"/> which inverts whatever is behind it in the application. +/// </summary> +public class BackdropInvertBrush : XamlCompositionEffectBrushBase +{ + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Invert(); + } +} diff --git a/components/Media/src/Brushes/BackdropSaturationBrush.cs b/components/Media/src/Brushes/BackdropSaturationBrush.cs new file mode 100644 index 00000000..f965bb2e --- /dev/null +++ b/components/Media/src/Brushes/BackdropSaturationBrush.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Brush which applies a SaturationEffect to the Backdrop. http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SaturationEffect.htm +/// </summary> +public class BackdropSaturationBrush : XamlCompositionEffectBrushBase +{ + /// <summary> + /// The <see cref="EffectSetter{T}"/> instance currently in use + /// </summary> + private EffectSetter<float>? setter; + + /// <summary> + /// Gets or sets the amount of gaussian blur to apply to the background. + /// </summary> + public double Saturation + { + get => (double)GetValue(SaturationProperty); + set => SetValue(SaturationProperty, value); + } + + /// <summary> + /// Identifies the <see cref="Saturation"/> dependency property. + /// </summary> + public static readonly DependencyProperty SaturationProperty = DependencyProperty.Register( + nameof(Saturation), + typeof(double), + typeof(BackdropSaturationBrush), + new PropertyMetadata(0.5, new PropertyChangedCallback(OnSaturationChanged))); + + /// <summary> + /// Updates the UI when <see cref="Saturation"/> changes + /// </summary> + /// <param name="d">The current <see cref="BackdropSaturationBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="SaturationProperty"/></param> + private static void OnSaturationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (BackdropSaturationBrush)d; + + // Clamp Value as per docs http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SaturationEffect.htm + var value = (float)(double)e.NewValue; + if (value > 1.0) + { + brush.Saturation = 1.0; + } + else if (value < 0.0) + { + brush.Saturation = 0.0; + } + + // Unbox and set a new blur amount if the CompositionBrush exists + if (brush.CompositionBrush is CompositionBrush target) + { + brush.setter?.Invoke(target, (float)brush.Saturation); + } + } + + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Saturation((float)Saturation, out setter); + } +} diff --git a/components/Media/src/Brushes/BackdropSepiaBrush.cs b/components/Media/src/Brushes/BackdropSepiaBrush.cs new file mode 100644 index 00000000..73f28a36 --- /dev/null +++ b/components/Media/src/Brushes/BackdropSepiaBrush.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Brush which applies a SepiaEffect to the Backdrop. http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SepiaEffect.htm +/// </summary> +public class BackdropSepiaBrush : XamlCompositionEffectBrushBase +{ + /// <summary> + /// The <see cref="EffectSetter{T}"/> instance currently in use + /// </summary> + private EffectSetter<float>? setter; + + /// <summary> + /// Gets or sets the amount of gaussian blur to apply to the background. + /// </summary> + public double Intensity + { + get => (double)GetValue(IntensityProperty); + set => SetValue(IntensityProperty, value); + } + + /// <summary> + /// Identifies the <see cref="Intensity"/> dependency property. + /// </summary> + public static readonly DependencyProperty IntensityProperty = DependencyProperty.Register( + nameof(Intensity), + typeof(double), + typeof(BackdropSepiaBrush), + new PropertyMetadata(0.5, new PropertyChangedCallback(OnIntensityChanged))); + + /// <summary> + /// Updates the UI when <see cref="Intensity"/> changes + /// </summary> + /// <param name="d">The current <see cref="BackdropSepiaBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="IntensityProperty"/></param> + private static void OnIntensityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (BackdropSepiaBrush)d; + + // Clamp Value as per docs http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SepiaEffect.htm + var value = (float)(double)e.NewValue; + if (value > 1.0) + { + brush.Intensity = 1.0; + } + else if (value < 0.0) + { + brush.Intensity = 0.0; + } + + // Unbox and set a new blur amount if the CompositionBrush exists. + if (brush.CompositionBrush is CompositionBrush target) + { + brush.setter?.Invoke(target, (float)brush.Intensity); + } + } + + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Sepia((float)Intensity, out setter); + } +} diff --git a/components/Media/src/Brushes/Base/XamlCompositionEffectBrushBase.cs b/components/Media/src/Brushes/Base/XamlCompositionEffectBrushBase.cs new file mode 100644 index 00000000..e4b6f921 --- /dev/null +++ b/components/Media/src/Brushes/Base/XamlCompositionEffectBrushBase.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A custom <see cref="XamlCompositionBrushBase"/> <see langword="class"/> that's ready to be used with a custom <see cref="PipelineBuilder"/> pipeline. +/// </summary> +public abstract class XamlCompositionEffectBrushBase : XamlCompositionBrushBase +{ + /// <summary> + /// The initialization <see cref="AsyncMutex"/> instance. + /// </summary> + private readonly AsyncMutex connectedMutex = new AsyncMutex(); + + /// <summary> + /// A method that builds and returns the <see cref="PipelineBuilder"/> pipeline to use in the current instance.<para/> + /// This method can also be used to store any needed <see cref="EffectSetter{T}"/> or <see cref="EffectAnimation{T}"/> + /// instances in local fields, for later use (they will need to be called upon <see cref="XamlCompositionBrushBase.CompositionBrush"/>). + /// </summary> + /// <returns>A <see cref="PipelineBuilder"/> instance to create the brush to display.</returns> + protected abstract PipelineBuilder OnPipelineRequested(); + + private bool isEnabled = true; + + /// <summary> + /// Gets or sets a value indicating whether the current brush is using the provided pipeline, or the fallback color. + /// </summary> + public bool IsEnabled + { + get => this.isEnabled; + set => this.OnEnabledToggled(value); + } + + /// <inheritdoc/> + protected override async void OnConnected() + { + using (await this.connectedMutex.LockAsync()) + { + if (CompositionBrush == null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + if (this.isEnabled) + { + CompositionBrush = await OnPipelineRequested().BuildAsync(); + } + else + { + CompositionBrush = await PipelineBuilder.FromColor(FallbackColor).BuildAsync(); + } + + OnCompositionBrushUpdated(); + } + } + + base.OnConnected(); + } + + /// <inheritdoc/> + protected override async void OnDisconnected() + { + using (await this.connectedMutex.LockAsync()) + { + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + + OnCompositionBrushUpdated(); + } + } + + base.OnDisconnected(); + } + + /// <summary> + /// Updates the <see cref="XamlCompositionBrushBase.CompositionBrush"/> property depending on the input value. + /// </summary> + /// <param name="value">The new value being set to the <see cref="IsEnabled"/> property.</param> + protected async void OnEnabledToggled(bool value) + { + using (await this.connectedMutex.LockAsync()) + { + if (this.isEnabled == value) + { + return; + } + + this.isEnabled = value; + + if (CompositionBrush != null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + if (this.isEnabled) + { + CompositionBrush = await OnPipelineRequested().BuildAsync(); + } + else + { + CompositionBrush = await PipelineBuilder.FromColor(FallbackColor).BuildAsync(); + } + + OnCompositionBrushUpdated(); + } + } + } + + /// <summary> + /// Invoked whenever the <see cref="XamlCompositionBrushBase.CompositionBrush"/> property is updated. + /// </summary> + protected virtual void OnCompositionBrushUpdated() + { + } +} diff --git a/components/Media/src/Brushes/CanvasBrushBase.cs b/components/Media/src/Brushes/CanvasBrushBase.cs new file mode 100644 index 00000000..55cd1be0 --- /dev/null +++ b/components/Media/src/Brushes/CanvasBrushBase.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.UI.Composition; + +#if WINUI2 +using Windows.UI.Composition; +using Windows.Graphics.DirectX; +#elif WINUI3 +using Microsoft.UI.Composition; +using Microsoft.Graphics.DirectX; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Helper Brush class to interop with Win2D Canvas calls. +/// </summary> +public abstract class CanvasBrushBase : XamlCompositionBrushBase +{ + private CompositionSurfaceBrush? _surfaceBrush; + + /// <summary> + /// Gets or sets the internal surface render width. Modify during construction. + /// </summary> + protected float SurfaceWidth { get; set; } + + /// <summary> + /// Gets or sets the internal surface render height. Modify during construction. + /// </summary> + protected float SurfaceHeight { get; set; } + + private CanvasDevice? _device; + + private CompositionGraphicsDevice? _graphics; + + /// <summary> + /// Implemented by parent class and called when canvas is being constructed for brush. + /// </summary> + /// <param name="device">Canvas device.</param> + /// <param name="session">Canvas drawing session.</param> + /// <param name="size">Size of surface to draw on.</param> + /// <returns>True if drawing was completed and the brush is ready, otherwise return False to not create brush yet.</returns> + protected abstract bool OnDraw(CanvasDevice device, CanvasDrawingSession session, Vector2 size); + + /// <summary> + /// Initializes the Composition Brush. + /// </summary> + protected override void OnConnected() + { + base.OnConnected(); + + if (_device != null) + { + _device.DeviceLost -= CanvasDevice_DeviceLost; + } + + _device = CanvasDevice.GetSharedDevice(); + _device.DeviceLost += CanvasDevice_DeviceLost; + + if (_graphics != null) + { + _graphics.RenderingDeviceReplaced -= CanvasDevice_RenderingDeviceReplaced; + } + + _graphics = CanvasComposition.CreateCompositionGraphicsDevice(Window.Current.Compositor, _device); + _graphics.RenderingDeviceReplaced += CanvasDevice_RenderingDeviceReplaced; + + // Delay creating composition resources until they're required. + if (CompositionBrush == null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + var size = new Vector2(SurfaceWidth, SurfaceHeight); + var surface = _graphics.CreateDrawingSurface(size.ToSize(), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + + using (var session = CanvasComposition.CreateDrawingSession(surface)) + { + // Call Implementor to draw on session. + if (!OnDraw(_device, session, size)) + { + return; + } + } + + _surfaceBrush = Window.Current.Compositor.CreateSurfaceBrush(surface); + _surfaceBrush.Stretch = CompositionStretch.Fill; + + CompositionBrush = _surfaceBrush; + } + } + + private void CanvasDevice_RenderingDeviceReplaced(CompositionGraphicsDevice sender, object args) + { + OnDisconnected(); + OnConnected(); + } + + private void CanvasDevice_DeviceLost(CanvasDevice sender, object args) + { + OnDisconnected(); + OnConnected(); + } + + /// <summary> + /// Deconstructs the Composition Brush. + /// </summary> + protected override void OnDisconnected() + { + base.OnDisconnected(); + + if (_device != null) + { + _device.DeviceLost -= CanvasDevice_DeviceLost; + _device = null; + } + + if (_graphics != null) + { + _graphics.RenderingDeviceReplaced -= CanvasDevice_RenderingDeviceReplaced; + _graphics = null; + } + + // Dispose of composition resources when no longer in use. + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + + if (_surfaceBrush != null) + { + _surfaceBrush.Dispose(); + _surfaceBrush = null; + } + } +} diff --git a/components/Media/src/Brushes/ImageBlendBrush.cs b/components/Media/src/Brushes/ImageBlendBrush.cs new file mode 100644 index 00000000..4f5123ba --- /dev/null +++ b/components/Media/src/Brushes/ImageBlendBrush.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Image loading reference from https://blogs.windows.com/buildingapps/2017/07/18/working-brushes-content-xaml-visual-layer-interop-part-one/#MA0k4EYWzqGKV501.97 + +using Microsoft.Graphics.Canvas.Effects; +using CanvasBlendEffect = Microsoft.Graphics.Canvas.Effects.BlendEffect; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Media.Imaging; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Media.Imaging; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Brush which blends a <see cref="BitmapImage"/> to the Backdrop in a given mode. See http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffect.htm. +/// </summary> +public class ImageBlendBrush : XamlCompositionBrushBase +{ + private LoadedImageSurface? _surface; + private CompositionSurfaceBrush? _surfaceBrush; + + /// <summary> + /// Gets or sets the <see cref="BitmapImage"/> source of the image to composite. + /// </summary> + public ImageSource Source + { + get => (ImageSource)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + /// <summary> + /// Identifies the <see cref="Source"/> dependency property. + /// </summary> + public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( + nameof(Source), + typeof(ImageSource), // We use ImageSource type so XAML engine will automatically construct proper object from String. + typeof(ImageBlendBrush), + new PropertyMetadata(null, OnImageSourceChanged)); + + /// <summary> + /// Gets or sets how to stretch the image within the brush. + /// </summary> + public Stretch Stretch + { + get => (Stretch)GetValue(StretchProperty); + set => SetValue(StretchProperty, value); + } + + /// <summary> + /// Identifies the <see cref="Stretch"/> dependency property. + /// Requires 16299 or higher for modes other than None. + /// </summary> + public static readonly DependencyProperty StretchProperty = DependencyProperty.Register( + nameof(Stretch), + typeof(Stretch), + typeof(ImageBlendBrush), + new PropertyMetadata(Stretch.None, OnStretchChanged)); + + /// <summary> + /// Gets or sets how to blend the image with the backdrop. + /// </summary> + public ImageBlendMode Mode + { + get => (ImageBlendMode)GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + /// <summary> + /// Identifies the <see cref="Mode"/> dependency property. + /// </summary> + public static readonly DependencyProperty ModeProperty = DependencyProperty.Register( + nameof(Mode), + typeof(ImageBlendMode), + typeof(ImageBlendBrush), + new PropertyMetadata(ImageBlendMode.Multiply, OnModeChanged)); + + private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (ImageBlendBrush)d; + + // Unbox and update surface if CompositionBrush exists + if (brush._surfaceBrush != null) + { + // If UriSource is invalid, StartLoadFromUri will return a blank texture. + var uri = (e.NewValue as BitmapImage)?.UriSource ?? new Uri("ms-appx:///"); + var newSurface = LoadedImageSurface.StartLoadFromUri(uri); + + brush._surface = newSurface; + brush._surfaceBrush.Surface = newSurface; + } + else + { + // If we didn't initially have a valid surface, we need to recreate our effect now. + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + private static void OnStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (ImageBlendBrush)d; + + // Unbox and update surface if CompositionBrush exists + if (brush._surfaceBrush != null) + { + // Modify the stretch property on our brush. + brush._surfaceBrush.Stretch = CompositionStretchFromStretch((Stretch)e.NewValue); + } + } + + private static void OnModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (ImageBlendBrush)d; + + // We can't animate our enum properties so recreate our internal brush. + brush.OnDisconnected(); + brush.OnConnected(); + } + + /// <inheritdoc/> + protected override void OnConnected() + { + // Delay creating composition resources until they're required. + if (CompositionBrush == null && Source != null && Source is BitmapImage bitmap) + { + // Use LoadedImageSurface API to get ICompositionSurface from image uri provided + // If UriSource is invalid, StartLoadFromUri will return a blank texture. + _surface = LoadedImageSurface.StartLoadFromUri(bitmap.UriSource); + + // Load Surface onto SurfaceBrush + _surfaceBrush = Window.Current.Compositor.CreateSurfaceBrush(_surface); + _surfaceBrush.Stretch = CompositionStretchFromStretch(Stretch); + +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + // Just use image straight-up, if we don't support effects. + CompositionBrush = _surfaceBrush; + return; + } + + var backdrop = Window.Current.Compositor.CreateBackdropBrush(); + + // Use a Win2D invert affect applied to a CompositionBackdropBrush. + var graphicsEffect = new CanvasBlendEffect + { + Name = "Invert", + Mode = (BlendEffectMode)(int)Mode, + Background = new CompositionEffectSourceParameter("backdrop"), + Foreground = new CompositionEffectSourceParameter("image") + }; + + var effectFactory = Window.Current.Compositor.CreateEffectFactory(graphicsEffect); + var effectBrush = effectFactory.CreateBrush(); + + effectBrush.SetSourceParameter("backdrop", backdrop); + effectBrush.SetSourceParameter("image", _surfaceBrush); + + CompositionBrush = effectBrush; + } + } + + /// <inheritdoc/> + protected override void OnDisconnected() + { + // Dispose of composition resources when no longer in use. + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + + if (_surfaceBrush != null) + { + _surfaceBrush.Dispose(); + _surfaceBrush = null; + } + + if (_surface != null) + { + _surface.Dispose(); + _surface = null; + } + } + + //// Helper to allow XAML developer to use XAML stretch property rather than another enum. + private static CompositionStretch CompositionStretchFromStretch(Stretch value) + { + switch (value) + { + case Stretch.None: + return CompositionStretch.None; + case Stretch.Fill: + return CompositionStretch.Fill; + case Stretch.Uniform: + return CompositionStretch.Uniform; + case Stretch.UniformToFill: + return CompositionStretch.UniformToFill; + } + + return CompositionStretch.None; + } +} diff --git a/components/Media/src/Brushes/PipelineBrush.cs b/components/Media/src/Brushes/PipelineBrush.cs new file mode 100644 index 00000000..8267c7dd --- /dev/null +++ b/components/Media/src/Brushes/PipelineBrush.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A <see cref="Brush"/> that renders a customizable Composition/Win2D effects pipeline +/// </summary> +[ContentProperty(Name = nameof(Effects))] +public sealed class PipelineBrush : XamlCompositionEffectBrushBase +{ + /// <summary> + /// Gets or sets the source for the current pipeline (defaults to a <see cref="BackdropSourceExtension"/> with <see cref="AcrylicBackgroundSource.Backdrop"/> source). + /// </summary> + public PipelineBuilder? Source { get; set; } + + /// <summary> + /// Gets or sets the collection of effects to use in the current pipeline. + /// </summary> + public IList<PipelineEffect> Effects + { + get + { + if (GetValue(EffectsProperty) is not IList<PipelineEffect> effects) + { + effects = new List<PipelineEffect>(); + + SetValue(EffectsProperty, effects); + } + + return effects; + } + set => SetValue(EffectsProperty, value); + } + + /// <summary> + /// Identifies the <seealso cref="Effects"/> dependency property. + /// </summary> + public static readonly DependencyProperty EffectsProperty = DependencyProperty.Register( + nameof(Effects), + typeof(IList<PipelineEffect>), + typeof(PipelineBrush), + new PropertyMetadata(null)); + + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + PipelineBuilder builder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + builder = effect.AppendToBuilder(builder); + } + + return builder; + } + + /// <inheritdoc/> + protected override void OnCompositionBrushUpdated() + { + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(CompositionBrush); + } + } +} diff --git a/components/Media/src/Brushes/TilesBrush.cs b/components/Media/src/Brushes/TilesBrush.cs new file mode 100644 index 00000000..894e98ab --- /dev/null +++ b/components/Media/src/Brushes/TilesBrush.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A <see cref="XamlCompositionBrush"/> that displays a tiled image +/// </summary> +public sealed class TilesBrush : XamlCompositionEffectBrushBase +{ + /// <summary> + /// Gets or sets the <see cref="Uri"/> to the texture to use + /// </summary> + public Uri TextureUri + { + get => (Uri)GetValue(TextureUriProperty); + set => SetValue(TextureUriProperty, value); + } + + /// <summary> + /// Identifies the <see cref="TextureUri"/> dependency property. + /// </summary> + public static readonly DependencyProperty TextureUriProperty = DependencyProperty.Register( + nameof(TextureUri), + typeof(Uri), + typeof(TilesBrush), + new PropertyMetadata(default, OnDependencyPropertyChanged)); + + /// <summary> + /// Gets or sets the DPI mode used to render the texture (the default is <see cref="Media.DpiMode.DisplayDpiWith96AsLowerBound"/>) + /// </summary> + public DpiMode DpiMode + { + get => (DpiMode)GetValue(DpiModeProperty); + set => SetValue(DpiModeProperty, value); + } + + /// <summary> + /// Identifies the <see cref="DpiMode"/> dependency property. + /// </summary> + public static readonly DependencyProperty DpiModeProperty = DependencyProperty.Register( + nameof(DpiMode), + typeof(DpiMode), + typeof(TilesBrush), + new PropertyMetadata(DpiMode.DisplayDpiWith96AsLowerBound, OnDependencyPropertyChanged)); + + /// <summary> + /// Updates the UI when either <see cref="TextureUri"/> or <see cref="DpiMode"/> changes + /// </summary> + /// <param name="d">The current <see cref="TilesBrush"/> instance</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for <see cref="TextureUriProperty"/> or <see cref="DpiModeProperty"/></param> + private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TilesBrush brush && + brush.CompositionBrush != null) + { + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + if (TextureUri is Uri uri) + { + return PipelineBuilder.FromTiles(uri, DpiMode); + } + + return PipelineBuilder.FromColor(default); + } +} \ No newline at end of file diff --git a/components/Media/src/Brushes/XamlCompositionBrush.cs b/components/Media/src/Brushes/XamlCompositionBrush.cs new file mode 100644 index 00000000..2b507da9 --- /dev/null +++ b/components/Media/src/Brushes/XamlCompositionBrush.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.System; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A <see langword="delegate"/> that represents a custom effect setter that can be applied to a <see cref="XamlCompositionBrush"/> instance +/// </summary> +/// <typeparam name="T">The type of property value to set</typeparam> +/// <param name="value">The effect target value</param> +public delegate void XamlEffectSetter<in T>(T value) + where T : unmanaged; + +/// <summary> +/// A <see langword="delegate"/> that represents a custom effect animation that can be applied to a <see cref="XamlCompositionBrush"/> instance +/// </summary> +/// <typeparam name="T">The type of property value to animate</typeparam> +/// <param name="value">The animation target value</param> +/// <param name="duration">The animation duration</param> +/// <returns>A <see cref="Task"/> that completes when the target animation completes</returns> +public delegate Task XamlEffectAnimation<in T>(T value, TimeSpan duration) + where T : unmanaged; + +/// <summary> +/// A simple <see langword="class"/> that can be used to quickly create XAML brushes from arbitrary <see cref="PipelineBuilder"/> pipelines +/// </summary> +public sealed class XamlCompositionBrush : XamlCompositionEffectBrushBase +{ + /// <summary> + /// Gets the <see cref="PipelineBuilder"/> pipeline for the current instance + /// </summary> + public PipelineBuilder Pipeline { get; } + + /// <summary> + /// Initializes a new instance of the <see cref="XamlCompositionBrush"/> class. + /// </summary> + /// <param name="pipeline">The <see cref="PipelineBuilder"/> instance to create the effect</param> + public XamlCompositionBrush(PipelineBuilder pipeline) => this.Pipeline = pipeline; + + /// <summary> + /// Binds an <see cref="EffectSetter{T}"/> to the composition brush in the current instance + /// </summary> + /// <typeparam name="T">The type of property value to set</typeparam> + /// <param name="setter">The input setter</param> + /// <param name="bound">The resulting setter</param> + /// <returns>The current <see cref="XamlCompositionBrush"/> instance</returns> + [Pure] + public XamlCompositionBrush Bind<T>(EffectSetter<T> setter, out XamlEffectSetter<T> bound) + where T : unmanaged + { + bound = value => setter(this.CompositionBrush, value); + + return this; + } + + /// <summary> + /// Binds an <see cref="EffectAnimation{T}"/> to the composition brush in the current instance + /// </summary> + /// <typeparam name="T">The type of property value to animate</typeparam> + /// <param name="animation">The input animation</param> + /// <param name="bound">The resulting animation</param> + /// <returns>The current <see cref="XamlCompositionBrush"/> instance</returns> + [Pure] + public XamlCompositionBrush Bind<T>(EffectAnimation<T> animation, out XamlEffectAnimation<T> bound) + where T : unmanaged + { + bound = (value, duration) => animation(this.CompositionBrush, value, duration); + + return this; + } + + /// <inheritdoc cref="XamlCompositionEffectBrushBase"/> + protected override PipelineBuilder OnPipelineRequested() => this.Pipeline; + + /// <summary> + /// Clones the current instance by rebuilding the source <see cref="Windows.UI.Xaml.Media.Brush"/>. Use this method to reuse the same effects pipeline on a different <see cref="DispatcherQueue"/> + /// </summary> + /// <remarks> + /// If your code is already on the same thread, you can just assign this brush to an arbitrary number of controls and it will still work correctly. + /// This method is only meant to be used to create a new instance of this brush using the same pipeline, on threads that can't access the current instance, for example in secondary app windows. + /// </remarks> + /// <returns>A <see cref="XamlCompositionBrush"/> instance using the current effects pipeline</returns> + [Pure] + public XamlCompositionBrush Clone() + { + return new XamlCompositionBrush(this.Pipeline); + } +} \ No newline at end of file diff --git a/components/Media/src/CommunityToolkit.WinUI.Media.csproj b/components/Media/src/CommunityToolkit.WinUI.Media.csproj new file mode 100644 index 00000000..dc295949 --- /dev/null +++ b/components/Media/src/CommunityToolkit.WinUI.Media.csproj @@ -0,0 +1,17 @@ +<Project Sdk="MSBuild.Sdk.Extras/3.0.23"> + <PropertyGroup> + <ToolkitComponentName>Media</ToolkitComponentName> + <Description>This package contains Media.</Description> + <Version>8.0.0-beta.1</Version> + + <!-- Rns suffix is required for namespaces shared across projects. See https://github.com/CommunityToolkit/Labs-Windows/issues/152 --> + <RootNamespace>CommunityToolkit.WinUI.MediaRns</RootNamespace> + </PropertyGroup> + + <!-- Sets this up as a toolkit component's source project --> + <Import Project="$(ToolingDirectory)\ToolkitComponent.SourceProject.props" /> + + <ItemGroup> + <ProjectReference Include="$(ToolkitAnimationSourceProject)" /> + </ItemGroup> +</Project> diff --git a/components/Media/src/Dependencies.props b/components/Media/src/Dependencies.props new file mode 100644 index 00000000..07187681 --- /dev/null +++ b/components/Media/src/Dependencies.props @@ -0,0 +1,22 @@ +<!-- + WinUI 2 under UWP uses TargetFramework uap10.0.* + WinUI 3 under WinAppSdk uses TargetFramework net6.0-windows10.* + However, under Uno-powered platforms, both WinUI 2 and 3 can share the same TargetFramework. + + MSBuild doesn't play nicely with this out of the box, so we've made it easy for you. + + For .NET Standard packages, you can use the Nuget Package Manager in Visual Studio. + For UWP / WinAppSDK / Uno packages, place the package references here. +--> +<Project> + <!-- WinUI 2 / UWP --> + <ItemGroup Condition="'$(IsUwp)' == 'true'"> + <PackageReference Include="Win2D.uwp" Version="1.26.0" /> + <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" /> + </ItemGroup> + + <!-- WinUI 3 / WinAppSdk --> + <ItemGroup Condition="'$(IsWinAppSdk)' == 'true'"> + <PackageReference Include="Microsoft.Graphics.Win2D" Version="1.0.5.1" /> + </ItemGroup> +</Project> diff --git a/components/Media/src/Effects/Abstract/ImageSourceBaseExtension.cs b/components/Media/src/Effects/Abstract/ImageSourceBaseExtension.cs new file mode 100644 index 00000000..011b6c0c --- /dev/null +++ b/components/Media/src/Effects/Abstract/ImageSourceBaseExtension.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An image based effect that loads an image at the specified location +/// </summary> +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public abstract class ImageSourceBaseExtension : MarkupExtension +{ + /// <summary> + /// Gets or sets the <see cref="System.Uri"/> for the image to load + /// </summary> + public Uri? Uri { get; set; } + + /// <summary> + /// Gets or sets the DPI mode used to render the image (the default is <see cref="Media.DpiMode.DisplayDpiWith96AsLowerBound"/>) + /// </summary> + public DpiMode DpiMode { get; set; } = DpiMode.DisplayDpiWith96AsLowerBound; + + /// <summary> + /// Gets or sets the cache mode to use when loading the image (the default is <see cref="Media.CacheMode.Default"/>) + /// </summary> + public CacheMode CacheMode { get; set; } = CacheMode.Default; +} diff --git a/components/Media/src/Effects/Abstract/PipelineEffect.cs b/components/Media/src/Effects/Abstract/PipelineEffect.cs new file mode 100644 index 00000000..0a59447f --- /dev/null +++ b/components/Media/src/Effects/Abstract/PipelineEffect.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A base pipeline effect. +/// </summary> +public abstract class PipelineEffect : DependencyObject, IPipelineEffect +{ + /// <inheritdoc/> + public CompositionBrush? Brush { get; private set; } + + /// <summary> + /// Gets or sets a value indicating whether the effect can be animated. + /// </summary> + public bool IsAnimatable { get; set; } + + /// <inheritdoc/> + public abstract PipelineBuilder AppendToBuilder(PipelineBuilder builder); + + /// <inheritdoc/> + public virtual void NotifyCompositionBrushInUse(CompositionBrush brush) + { + Brush = brush; + } +} diff --git a/components/Media/src/Effects/BlendEffect.cs b/components/Media/src/Effects/BlendEffect.cs new file mode 100644 index 00000000..04933d3d --- /dev/null +++ b/components/Media/src/Effects/BlendEffect.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A blend effect that merges the current builder with an input one +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.BlendEffect"/> effect</remarks> +[ContentProperty(Name = nameof(Effects))] +public sealed class BlendEffect : PipelineEffect +{ + /// <summary> + /// Gets or sets the input to merge with the current instance (defaults to a <see cref="BackdropSourceExtension"/> with <see cref="Windows.UI.Xaml.Media.AcrylicBackgroundSource.Backdrop"/> source). + /// </summary> + public PipelineBuilder? Source { get; set; } + + /// <summary> + /// Gets or sets the effects to apply to the input to merge with the current instance + /// </summary> + public List<IPipelineEffect> Effects { get; set; } = new List<IPipelineEffect>(); + + /// <summary> + /// Gets or sets the blending mode to use (the default mode is <see cref="ImageBlendMode.Multiply"/>) + /// </summary> + public ImageBlendMode Mode { get; set; } + + /// <summary> + /// Gets or sets the placement of the input builder with respect to the current one (the default is <see cref="Media.Placement.Foreground"/>) + /// </summary> + public Placement Placement { get; set; } = Placement.Foreground; + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + PipelineBuilder inputBuilder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + inputBuilder = effect.AppendToBuilder(inputBuilder); + } + + return builder.Blend(inputBuilder, (BlendEffectMode)Mode, Placement); + } + + /// <inheritdoc/> + public override void NotifyCompositionBrushInUse(CompositionBrush brush) + { + base.NotifyCompositionBrushInUse(brush); + + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(brush); + } + } +} diff --git a/components/Media/src/Effects/BlurEffect.cs b/components/Media/src/Effects/BlurEffect.cs new file mode 100644 index 00000000..42284934 --- /dev/null +++ b/components/Media/src/Effects/BlurEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A gaussian blur effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.GaussianBlurEffect"/> effect</remarks> +public sealed class BlurEffect : PipelineEffect +{ + private double amount; + + /// <summary> + /// Gets or sets the blur amount for the effect (must be a positive value) + /// </summary> + public double Amount + { + get => this.amount; + set => this.amount = Math.Max(value, 0); + } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Blur((float)Amount, out string id); + + Id = id; + + return builder; + } + + return builder.Blur((float)Amount); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/CrossFadeEffect.cs b/components/Media/src/Effects/CrossFadeEffect.cs new file mode 100644 index 00000000..84210ab0 --- /dev/null +++ b/components/Media/src/Effects/CrossFadeEffect.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A blend effect that merges the current builder with an input one +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.CrossFadeEffect"/> effect</remarks> +[ContentProperty(Name = nameof(Effects))] +public sealed class CrossFadeEffect : PipelineEffect +{ + /// <summary> + /// Gets or sets the input to merge with the current instance (defaults to a <see cref="BackdropSourceExtension"/> with <see cref="Windows.UI.Xaml.Media.AcrylicBackgroundSource.Backdrop"/> source). + /// </summary> + public PipelineBuilder? Source { get; set; } + + /// <summary> + /// Gets or sets the effects to apply to the input to merge with the current instance + /// </summary> + public List<IPipelineEffect> Effects { get; set; } = new List<IPipelineEffect>(); + + private double factor = 0.5; + + /// <summary> + /// Gets or sets the The cross fade factor to blend the input effects (default to 0.5, should be in the [0, 1] range) + /// </summary> + public double Factor + { + get => this.factor; + set => this.factor = Math.Clamp(value, 0, 1); + } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + PipelineBuilder inputBuilder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + inputBuilder = effect.AppendToBuilder(inputBuilder); + } + + if (IsAnimatable) + { + builder = builder.CrossFade(inputBuilder, (float)Factor, out string id); + + Id = id; + + return builder; + } + + return builder.CrossFade(inputBuilder, (float)Factor); + } + + /// <inheritdoc/> + public override void NotifyCompositionBrushInUse(CompositionBrush brush) + { + base.NotifyCompositionBrushInUse(brush); + + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(brush); + } + } +} diff --git a/components/Media/src/Effects/ExposureEffect.cs b/components/Media/src/Effects/ExposureEffect.cs new file mode 100644 index 00000000..5f19ac8e --- /dev/null +++ b/components/Media/src/Effects/ExposureEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An exposure effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.ExposureEffect"/> effect</remarks> +public sealed class ExposureEffect : PipelineEffect +{ + private double amount; + + /// <summary> + /// Gets or sets the amount of exposure to apply to the background (defaults to 0, should be in the [-2, 2] range). + /// </summary> + public double Amount + { + get => this.amount; + set => this.amount = Math.Clamp(value, -2, 2); + } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Exposure((float)Amount, out string id); + + Id = id; + + return builder; + } + + return builder.Exposure((float)Amount); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/Extensions/AcrylicSourceExtension.cs b/components/Media/src/Effects/Extensions/AcrylicSourceExtension.cs new file mode 100644 index 00000000..cb555ab2 --- /dev/null +++ b/components/Media/src/Effects/Extensions/AcrylicSourceExtension.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINUI2 + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A custom acrylic effect that can be inserted into a pipeline +/// </summary> +/// <remarks>This effect mirrors the look of the default <see cref="AcrylicBrush"/> implementation</remarks> +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public sealed class AcrylicSourceExtension : MarkupExtension +{ + /// <summary> + /// Gets or sets the background source mode for the effect (the default is <see cref="AcrylicBackgroundSource.Backdrop"/>). + /// </summary> + public AcrylicBackgroundSource BackgroundSource { get; set; } = AcrylicBackgroundSource.Backdrop; + + private double blurAmount; + + /// <summary> + /// Gets or sets the blur amount for the effect (must be a positive value) + /// </summary> + /// <remarks>This property is ignored when the active mode is <see cref="AcrylicBackgroundSource.HostBackdrop"/></remarks> + public double BlurAmount + { + get => this.blurAmount; + set => this.blurAmount = Math.Max(value, 0); + } + + /// <summary> + /// Gets or sets the tint for the effect + /// </summary> + public Color TintColor { get; set; } + + private double tintOpacity = 0.5f; + + /// <summary> + /// Gets or sets the color for the tint effect (default is 0.5, must be in the [0, 1] range) + /// </summary> + public double TintOpacity + { + get => this.tintOpacity; + set => this.tintOpacity = Math.Clamp(value, 0, 1); + } + + /// <summary> + /// Gets or sets the <see cref="Uri"/> to the texture to use + /// </summary> + public Uri? TextureUri { get; set; } + + /// <inheritdoc/> + protected override object ProvideValue() + { + return BackgroundSource switch + { + AcrylicBackgroundSource.Backdrop => PipelineBuilder.FromBackdropAcrylic(this.TintColor, (float)this.TintOpacity, (float)BlurAmount, TextureUri), + AcrylicBackgroundSource.HostBackdrop => PipelineBuilder.FromHostBackdropAcrylic(this.TintColor, (float)this.TintOpacity, TextureUri), + _ => throw new ArgumentException($"Invalid source mode for acrylic effect: {BackgroundSource}") + }; + } +} + +#endif diff --git a/components/Media/src/Effects/Extensions/BackdropSourceExtension.cs b/components/Media/src/Effects/Extensions/BackdropSourceExtension.cs new file mode 100644 index 00000000..6ed06966 --- /dev/null +++ b/components/Media/src/Effects/Extensions/BackdropSourceExtension.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A backdrop effect that can sample from a specified source +/// </summary> +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public sealed class BackdropSourceExtension : MarkupExtension +{ + /// <summary> + /// Gets or sets the background source mode for the effect (the default is <see cref="AcrylicBackgroundSource.Backdrop"/>). + /// </summary> + public AcrylicBackgroundSource BackgroundSource { get; set; } = AcrylicBackgroundSource.Backdrop; + + /// <inheritdoc/> + protected override object ProvideValue() + { + return BackgroundSource switch + { + AcrylicBackgroundSource.Backdrop => PipelineBuilder.FromBackdrop(), + AcrylicBackgroundSource.HostBackdrop => PipelineBuilder.FromHostBackdrop(), + _ => throw new ArgumentException($"Invalid source for backdrop effect: {BackgroundSource}") + }; + } +} +#endif diff --git a/components/Media/src/Effects/Extensions/ImageSourceExtension.cs b/components/Media/src/Effects/Extensions/ImageSourceExtension.cs new file mode 100644 index 00000000..8c04b7a9 --- /dev/null +++ b/components/Media/src/Effects/Extensions/ImageSourceExtension.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An image effect, which displays an image loaded as a Win2D surface +/// </summary> +public sealed class ImageSourceExtension : ImageSourceBaseExtension +{ + /// <inheritdoc/> + protected override object ProvideValue() + { + default(ArgumentNullException).ThrowIfNull(Uri); + return PipelineBuilder.FromImage(Uri, DpiMode, CacheMode); + } +} diff --git a/components/Media/src/Effects/Extensions/SolidColorSourceExtension.cs b/components/Media/src/Effects/Extensions/SolidColorSourceExtension.cs new file mode 100644 index 00000000..1f713f44 --- /dev/null +++ b/components/Media/src/Effects/Extensions/SolidColorSourceExtension.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An effect that renders a standard 8bit SDR color on the available surface +/// </summary> +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public sealed class SolidColorSourceExtension : MarkupExtension +{ + /// <summary> + /// Gets or sets the color to display + /// </summary> + public Color Color { get; set; } + + /// <inheritdoc/> + protected override object ProvideValue() + { + return PipelineBuilder.FromColor(Color); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/Extensions/TileSourceExtension.cs b/components/Media/src/Effects/Extensions/TileSourceExtension.cs new file mode 100644 index 00000000..a68956ee --- /dev/null +++ b/components/Media/src/Effects/Extensions/TileSourceExtension.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An effect that loads an image and replicates it to cover all the available surface area +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.BorderEffect"/> effect</remarks> +public sealed class TileSourceExtension : ImageSourceBaseExtension +{ + /// <inheritdoc/> + protected override object ProvideValue() + { + default(ArgumentNullException).ThrowIfNull(Uri); + return PipelineBuilder.FromTiles(Uri, DpiMode, CacheMode); + } +} diff --git a/components/Media/src/Effects/GrayscaleEffect.cs b/components/Media/src/Effects/GrayscaleEffect.cs new file mode 100644 index 00000000..c9f5a943 --- /dev/null +++ b/components/Media/src/Effects/GrayscaleEffect.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A grayscale effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.GrayscaleEffect"/> effect</remarks> +public sealed class GrayscaleEffect : PipelineEffect +{ + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.Grayscale(); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/HueRotationEffect.cs b/components/Media/src/Effects/HueRotationEffect.cs new file mode 100644 index 00000000..2b4d80b3 --- /dev/null +++ b/components/Media/src/Effects/HueRotationEffect.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A hue rotation effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.HueRotationEffect"/> effect</remarks> +public sealed class HueRotationEffect : PipelineEffect +{ + /// <summary> + /// Gets or sets the angle to rotate the hue, in radians + /// </summary> + public double Angle { get; set; } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.HueRotation((float)Angle, out string id); + + Id = id; + + return builder; + } + + return builder.HueRotation((float)Angle); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/Interfaces/IPipelineEffect.cs b/components/Media/src/Effects/Interfaces/IPipelineEffect.cs new file mode 100644 index 00000000..e93a5a00 --- /dev/null +++ b/components/Media/src/Effects/Interfaces/IPipelineEffect.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// The base <see langword="interface"/> for all the builder effects to be used in a <see cref="CompositionBrush"/>. +/// </summary> +public interface IPipelineEffect +{ + /// <summary> + /// Gets the current <see cref="CompositionBrush"/> instance, if one is in use. + /// </summary> + CompositionBrush? Brush { get; } + + /// <summary> + /// Appends the current effect to the input <see cref="PipelineBuilder"/> instance. + /// </summary> + /// <param name="builder">The source <see cref="PipelineBuilder"/> instance to add the effect to.</param> + /// <returns>A new <see cref="PipelineBuilder"/> with the new effects added to it.</returns> + PipelineBuilder AppendToBuilder(PipelineBuilder builder); + + /// <summary> + /// Notifies that a given <see cref="CompositionBrush"/> is now in use. + /// </summary> + /// <param name="brush">The <see cref="CompositionBrush"/> in use.</param> + void NotifyCompositionBrushInUse(CompositionBrush brush); +} diff --git a/components/Media/src/Effects/InvertEffect.cs b/components/Media/src/Effects/InvertEffect.cs new file mode 100644 index 00000000..9ec5437b --- /dev/null +++ b/components/Media/src/Effects/InvertEffect.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An color inversion effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.InvertEffect"/> effect</remarks> +public sealed class InvertEffect : PipelineEffect +{ + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.Invert(); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/LuminanceToAlphaEffect.cs b/components/Media/src/Effects/LuminanceToAlphaEffect.cs new file mode 100644 index 00000000..789f9829 --- /dev/null +++ b/components/Media/src/Effects/LuminanceToAlphaEffect.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A luminance to alpha effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.LuminanceToAlphaEffect"/> effect</remarks> +public sealed class LuminanceToAlphaEffect : PipelineEffect +{ + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.LuminanceToAlpha(); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/OpacityEffect.cs b/components/Media/src/Effects/OpacityEffect.cs new file mode 100644 index 00000000..e4696415 --- /dev/null +++ b/components/Media/src/Effects/OpacityEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An opacity effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.OpacityEffect"/> effect</remarks> +public sealed class OpacityEffect : PipelineEffect +{ + private double value = 1; + + /// <summary> + /// Gets or sets the opacity value to apply to the background (defaults to 1, should be in the [0, 1] range). + /// </summary> + public double Value + { + get => this.value; + set => this.value = Math.Clamp(value, 0, 1); + } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Opacity((float)Value, out string id); + + Id = id; + + return builder; + } + + return builder.Opacity((float)Value); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/SaturationEffect.cs b/components/Media/src/Effects/SaturationEffect.cs new file mode 100644 index 00000000..83ac5d18 --- /dev/null +++ b/components/Media/src/Effects/SaturationEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A saturation effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.SaturationEffect"/> effect</remarks> +public sealed class SaturationEffect : PipelineEffect +{ + private double value = 1; + + /// <summary> + /// Gets or sets the saturation amount to apply to the background (defaults to 1, should be in the [0, 1] range). + /// </summary> + public double Value + { + get => this.value; + set => this.value = Math.Clamp(value, 0, 1); + } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Saturation((float)Value, out string id); + + Id = id; + + return builder; + } + + return builder.Saturation((float)Value); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/SepiaEffect.cs b/components/Media/src/Effects/SepiaEffect.cs new file mode 100644 index 00000000..e393ea67 --- /dev/null +++ b/components/Media/src/Effects/SepiaEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A sepia effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.SepiaEffect"/> effect</remarks> +public sealed class SepiaEffect : PipelineEffect +{ + private double intensity = 0.5; + + /// <summary> + /// Gets or sets the intensity of the effect (defaults to 0.5, should be in the [0, 1] range). + /// </summary> + public double Intensity + { + get => this.intensity; + set => this.intensity = Math.Clamp(value, 0, 1); + } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Sepia((float)Intensity, out string id); + + Id = id; + + return builder; + } + + return builder.Sepia((float)Intensity); + } +} diff --git a/components/Media/src/Effects/ShadeEffect.cs b/components/Media/src/Effects/ShadeEffect.cs new file mode 100644 index 00000000..e56169b6 --- /dev/null +++ b/components/Media/src/Effects/ShadeEffect.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An effect that overlays a color layer over the current builder, with a specified intensity +/// </summary> +public sealed class ShadeEffect : PipelineEffect +{ + /// <summary> + /// Gets or sets the color to use + /// </summary> + public Color Color { get; set; } + + private double intensity = 0.5; + + /// <summary> + /// Gets or sets the intensity of the color layer (default to 0.5, should be in the [0, 1] range) + /// </summary> + public double Intensity + { + get => this.intensity; + set => this.intensity = Math.Clamp(value, 0, 1); + } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.Shade(Color, (float)Intensity); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/TemperatureAndTintEffect.cs b/components/Media/src/Effects/TemperatureAndTintEffect.cs new file mode 100644 index 00000000..385fb237 --- /dev/null +++ b/components/Media/src/Effects/TemperatureAndTintEffect.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A temperature and tint effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.TemperatureAndTintEffect"/> effect</remarks> +public sealed class TemperatureAndTintEffect : PipelineEffect +{ + private double temperature; + + /// <summary> + /// Gets or sets the value of the temperature for the current effect (defaults to 0, should be in the [-1, 1] range) + /// </summary> + public double Temperature + { + get => this.temperature; + set => this.temperature = Math.Clamp(value, -1, 1); + } + + private double tint; + + /// <summary> + /// Gets or sets the value of the tint for the current effect (defaults to 0, should be in the [-1, 1] range) + /// </summary> + public double Tint + { + get => this.tint; + set => this.tint = Math.Clamp(value, -1, 1); + } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.TemperatureAndTint((float)Temperature, (float)Tint); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/TintEffect.cs b/components/Media/src/Effects/TintEffect.cs new file mode 100644 index 00000000..e986481e --- /dev/null +++ b/components/Media/src/Effects/TintEffect.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A tint effect +/// </summary> +/// <remarks>This effect maps to the Win2D <see cref="Graphics.Canvas.Effects.TintEffect"/> effect</remarks> +public sealed class TintEffect : PipelineEffect +{ + /// <summary> + /// Gets or sets the int color to use + /// </summary> + public Color Color { get; set; } + + /// <summary> + /// Gets the unique id for the effect, if <see cref="PipelineEffect.IsAnimatable"/> is set. + /// </summary> + internal string? Id { get; private set; } + + /// <inheritdoc/> + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Tint(Color, out string id); + + Id = id; + + return builder; + } + + return builder.Tint(Color); + } +} \ No newline at end of file diff --git a/components/Media/src/Enums/AlphaMode.cs b/components/Media/src/Enums/AlphaMode.cs new file mode 100644 index 00000000..1cfa2b31 --- /dev/null +++ b/components/Media/src/Enums/AlphaMode.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Specifies the way in which an alpha channel affects color channels. +/// </summary> +public enum AlphaMode +{ + /// <summary> + /// Provides better transparent effects without a white bloom. + /// </summary> + Premultiplied = 0, + + /// <summary> + /// WPF default handling of alpha channel during transparent blending. + /// </summary> + Straight = 1, +} \ No newline at end of file diff --git a/components/Media/src/Enums/CacheMode.cs b/components/Media/src/Enums/CacheMode.cs new file mode 100644 index 00000000..57153b0b --- /dev/null +++ b/components/Media/src/Enums/CacheMode.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Indicates the cache mode to use when loading a Win2D image +/// </summary> +public enum CacheMode +{ + /// <summary> + /// The default behavior, the cache is enabled + /// </summary> + Default, + + /// <summary> + /// Reload the target image and overwrite the cached entry, if it exists + /// </summary> + Overwrite, + + /// <summary> + /// The cache is disabled and new images are always reloaded + /// </summary> + Disabled +} \ No newline at end of file diff --git a/components/Media/src/Enums/DpiMode.cs b/components/Media/src/Enums/DpiMode.cs new file mode 100644 index 00000000..4167eac9 --- /dev/null +++ b/components/Media/src/Enums/DpiMode.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Indicates the DPI mode to use to load an image +/// </summary> +public enum DpiMode +{ + /// <summary> + /// Uses the original DPI settings of the loaded image + /// </summary> + UseSourceDpi, + + /// <summary> + /// Uses the default value of 96 DPI + /// </summary> + Default96Dpi, + + /// <summary> + /// Overrides the image DPI settings with the current screen DPI value + /// </summary> + DisplayDpi, + + /// <summary> + /// Overrides the image DPI settings with the current screen DPI value and ensures the resulting value is at least 96 + /// </summary> + DisplayDpiWith96AsLowerBound +} \ No newline at end of file diff --git a/components/Media/src/Enums/ImageBlendMode.cs b/components/Media/src/Enums/ImageBlendMode.cs new file mode 100644 index 00000000..28d997f5 --- /dev/null +++ b/components/Media/src/Enums/ImageBlendMode.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Composition supported version of http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffectMode.htm. + +using Microsoft.Graphics.Canvas.Effects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - see http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffectMode.htm. + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Blend mode to use when compositing effects. +/// See http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffectMode.htm for details. +/// Dissolve is not supported. +/// </summary> +public enum ImageBlendMode +{ + Multiply = BlendEffectMode.Multiply, + Screen = BlendEffectMode.Screen, + Darken = BlendEffectMode.Darken, + Lighten = BlendEffectMode.Lighten, + ColorBurn = BlendEffectMode.ColorBurn, + LinearBurn = BlendEffectMode.LinearBurn, + DarkerColor = BlendEffectMode.DarkerColor, + LighterColor = BlendEffectMode.LighterColor, + ColorDodge = BlendEffectMode.ColorDodge, + LinearDodge = BlendEffectMode.LinearDodge, + Overlay = BlendEffectMode.Overlay, + SoftLight = BlendEffectMode.SoftLight, + HardLight = BlendEffectMode.HardLight, + VividLight = BlendEffectMode.VividLight, + LinearLight = BlendEffectMode.LinearLight, + PinLight = BlendEffectMode.PinLight, + HardMix = BlendEffectMode.HardMix, + Difference = BlendEffectMode.Difference, + Exclusion = BlendEffectMode.Exclusion, + + /// <summary> + /// Hue blend mode. + /// </summary> + Hue = BlendEffectMode.Hue, + + /// <summary> + /// Saturation blend mode. + /// </summary> + Saturation = BlendEffectMode.Saturation, + + /// <summary> + /// Color blend mode. + /// </summary> + Color = BlendEffectMode.Color, + + /// <summary> + /// Luminosity blend mode. + /// </summary> + Luminosity = BlendEffectMode.Luminosity, + Subtract = BlendEffectMode.Subtract, + Division = BlendEffectMode.Division, +} \ No newline at end of file diff --git a/components/Media/src/Enums/InnerContentClipMode.cs b/components/Media/src/Enums/InnerContentClipMode.cs new file mode 100644 index 00000000..a154ebb6 --- /dev/null +++ b/components/Media/src/Enums/InnerContentClipMode.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// The method that each instance of <see cref="AttachedCardShadow"/> uses when clipping its inner content. +/// </summary> +public enum InnerContentClipMode +{ + /// <summary> + /// Do not clip inner content. + /// </summary> + None, + + /// <summary> + /// Use <see cref="Windows.UI.Composition.CompositionMaskBrush"/> to clip inner content. + /// </summary> + /// <remarks> + /// This mode has better performance than <see cref="CompositionGeometricClip"/>. + /// </remarks> + CompositionMaskBrush, + + /// <summary> + /// Use <see cref="Windows.UI.Composition.CompositionGeometricClip"/> to clip inner content. + /// </summary> + /// <remarks> + /// Content clipped in this mode will have smoother corners than when using <see cref="CompositionMaskBrush"/>. + /// </remarks> + CompositionGeometricClip +} diff --git a/components/Media/src/Enums/Placement.cs b/components/Media/src/Enums/Placement.cs new file mode 100644 index 00000000..b2c6854c --- /dev/null +++ b/components/Media/src/Enums/Placement.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.Graphics.Effects; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An <see langword="enum"/> used to modify the default placement of the input <see cref="IGraphicsEffectSource"/> instance in a blend operation +/// </summary> +public enum Placement +{ + /// <summary> + /// The instance used to call the blend method is placed on top of the other + /// </summary> + Foreground, + + /// <summary> + /// The instance used to call the blend method is placed behind the other + /// </summary> + Background +} \ No newline at end of file diff --git a/components/Media/src/Extensions/System.Collections.Generic/GenericExtensions.cs b/components/Media/src/Extensions/System.Collections.Generic/GenericExtensions.cs new file mode 100644 index 00000000..ce09a05f --- /dev/null +++ b/components/Media/src/Extensions/System.Collections.Generic/GenericExtensions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An extension <see langword="class"/> for the <see cref="System.Collections.Generic"/> <see langword="namespace"/> +/// </summary> +internal static class GenericExtensions +{ + /// <summary> + /// Merges the two input <see cref="IReadOnlyDictionary{TKey,TValue}"/> instances and makes sure no duplicate keys are present + /// </summary> + /// <typeparam name="TKey">The type of keys in the input dictionaries</typeparam> + /// <typeparam name="TValue">The type of values in the input dictionaries</typeparam> + /// <param name="a">The first <see cref="IReadOnlyDictionary{TKey,TValue}"/> to merge</param> + /// <param name="b">The second <see cref="IReadOnlyDictionary{TKey,TValue}"/> to merge</param> + /// <returns>An <see cref="IReadOnlyDictionary{TKey,TValue}"/> instance with elements from both <paramref name="a"/> and <paramref name="b"/></returns> + [Pure] + public static IReadOnlyDictionary<TKey, TValue> Merge<TKey, TValue>( + this IReadOnlyDictionary<TKey, TValue> a, + IReadOnlyDictionary<TKey, TValue> b) + where TKey : notnull + { + if (a.Keys.FirstOrDefault(b.ContainsKey) is TKey key) + { + throw new InvalidOperationException($"The key {key} already exists in the current pipeline"); + } + + return new Dictionary<TKey, TValue>(a.Concat(b)); + } + + /// <summary> + /// Merges the two input <see cref="IReadOnlyCollection{T}"/> instances and makes sure no duplicate items are present + /// </summary> + /// <typeparam name="T">The type of elements in the input collections</typeparam> + /// <param name="a">The first <see cref="IReadOnlyCollection{T}"/> to merge</param> + /// <param name="b">The second <see cref="IReadOnlyCollection{T}"/> to merge</param> + /// <returns>An <see cref="IReadOnlyCollection{T}"/> instance with elements from both <paramref name="a"/> and <paramref name="b"/></returns> + [Pure] + public static IReadOnlyCollection<T> Merge<T>(this IReadOnlyCollection<T> a, IReadOnlyCollection<T> b) + { + if (a.Any(b.Contains)) + { + throw new InvalidOperationException("The input collection has at least an item already present in the second collection"); + } + + return a.Concat(b).ToArray(); + } +} diff --git a/components/Media/src/Extensions/System.Threading.Tasks/AsyncMutex.cs b/components/Media/src/Extensions/System.Threading.Tasks/AsyncMutex.cs new file mode 100644 index 00000000..d30e29ac --- /dev/null +++ b/components/Media/src/Extensions/System.Threading.Tasks/AsyncMutex.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An <see langword="async"/> <see cref="AsyncMutex"/> implementation that can be easily used inside a <see langword="using"/> block +/// </summary> +#pragma warning disable CA1001 // Types that own disposable fields should be disposable +internal sealed class AsyncMutex +#pragma warning restore CA1001 // Types that own disposable fields should be disposable +{ + /// <summary> + /// The underlying <see cref="SemaphoreSlim"/> instance in use + /// </summary> + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); + + /// <summary> + /// Acquires a lock for the current instance, that is automatically released outside the <see langword="using"/> block + /// </summary> + /// <returns>A <see cref="Task{T}"/> that returns an <see cref="IDisposable"/> instance to release the lock</returns> + public async Task<IDisposable> LockAsync() + { + await this.semaphore.WaitAsync().ConfigureAwait(false); + + return new Lock(this.semaphore); + } + + /// <summary> + /// Private class that implements the automatic release of the semaphore + /// </summary> + private sealed class Lock : IDisposable + { + /// <summary> + /// The <see cref="SemaphoreSlim"/> instance of the parent class + /// </summary> + private readonly SemaphoreSlim semaphore; + + /// <summary> + /// Initializes a new instance of the <see cref="Lock"/> class. + /// </summary> + /// <param name="semaphore">The <see cref="SemaphoreSlim"/> instance of the parent class</param> + public Lock(SemaphoreSlim semaphore) + { + this.semaphore = semaphore; + } + + /// <inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IDisposable.Dispose() + { + this.semaphore.Release(); + } + } +} diff --git a/components/Media/src/Extensions/System/UriExtensions.cs b/components/Media/src/Extensions/System/UriExtensions.cs new file mode 100644 index 00000000..71c20e80 --- /dev/null +++ b/components/Media/src/Extensions/System/UriExtensions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An extension <see langword="class"/> for the <see cref="Uri"/> type +/// </summary> +internal static class UriExtensions +{ + /// <summary> + /// Returns an <see cref="Uri"/> that starts with the ms-appx:// prefix + /// </summary> + /// <param name="uri">The input <see cref="Uri"/> to process</param> + /// <returns>A <see cref="Uri"/> equivalent to the first but relative to ms-appx://</returns> + /// <remarks>This is needed because the XAML converter doesn't use the ms-appx:// prefix</remarks> + [Pure] + public static Uri ToAppxUri(this Uri uri) + { + if (uri.Scheme.Equals("ms-resource")) + { + string path = uri.AbsolutePath.StartsWith("/Files") + ? uri.AbsolutePath.Replace("/Files", string.Empty) + : uri.AbsolutePath; + + return new Uri($"ms-appx://{path}"); + } + + return uri; + } + + /// <summary> + /// Returns an <see cref="Uri"/> that starts with the ms-appx:// prefix + /// </summary> + /// <param name="path">The input relative path to convert</param> + /// <returns>A <see cref="Uri"/> with <paramref name="path"/> relative to ms-appx://</returns> + [Pure] + public static Uri ToAppxUri(this string path) + { + string prefix = $"ms-appx://{(path.StartsWith('/') ? string.Empty : "/")}"; + + return new Uri($"{prefix}{path}"); + } +} \ No newline at end of file diff --git a/components/Media/src/Extensions/UIElementExtensions.cs b/components/Media/src/Extensions/UIElementExtensions.cs new file mode 100644 index 00000000..569fa16f --- /dev/null +++ b/components/Media/src/Extensions/UIElementExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// Attached properties to support attaching custom pipelines to UI elements. +/// </summary> +public static class UIElementExtensions +{ + /// <summary> + /// Identifies the VisualFactory XAML attached property. + /// </summary> + public static readonly DependencyProperty VisualFactoryProperty = DependencyProperty.RegisterAttached( + "VisualFactory", + typeof(AttachedVisualFactoryBase), + typeof(UIElementExtensions), + new PropertyMetadata(null, OnVisualFactoryPropertyChanged)); + + /// <summary> + /// Gets the value of <see cref="VisualFactoryProperty"/>. + /// </summary> + /// <param name="element">The <see cref="UIElement"/> to get the value for.</param> + /// <returns>The retrieved <see cref="AttachedVisualFactoryBase"/> item.</returns> + public static AttachedVisualFactoryBase GetVisualFactory(UIElement element) + { + return (AttachedVisualFactoryBase)element.GetValue(VisualFactoryProperty); + } + + /// <summary> + /// Sets the value of <see cref="VisualFactoryProperty"/>. + /// </summary> + /// <param name="element">The <see cref="UIElement"/> to set the value for.</param> + /// <param name="value">The <see cref="AttachedVisualFactoryBase"/> value to set.</param> + public static void SetVisualFactory(UIElement element, AttachedVisualFactoryBase value) + { + element.SetValue(VisualFactoryProperty, value); + } + + /// <summary> + /// Callback to apply the visual for <see cref="VisualFactoryProperty"/>. + /// </summary> + /// <param name="d">The target object the property was changed for.</param> + /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for the current event.</param> + private static async void OnVisualFactoryPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UIElement element = (UIElement)d; + Visual attachedVisual = await ((AttachedVisualFactoryBase)e.NewValue).GetAttachedVisualAsync(element); + + attachedVisual.RelativeSizeAdjustment = Vector2.One; + + ElementCompositionPreview.SetElementChildVisual(element, attachedVisual); + } +} diff --git a/components/Media/src/Extensions/Windows.UI.Composition/CompositionObjectExtensions.cs b/components/Media/src/Extensions/Windows.UI.Composition/CompositionObjectExtensions.cs new file mode 100644 index 00000000..195b0ce3 --- /dev/null +++ b/components/Media/src/Extensions/Windows.UI.Composition/CompositionObjectExtensions.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Windows.UI; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// An extension <see langword="class"/> for the <see cref="CompositionObject"/> type +/// </summary> +internal static class CompositionObjectExtensions +{ + /// <summary> + /// Starts an <see cref="ExpressionAnimation"/> to keep the size of the source <see cref="Visual"/> in sync with the target <see cref="UIElement"/> + /// </summary> + /// <param name="source">The <see cref="Visual"/> to start the animation on</param> + /// <param name="target">The target <see cref="UIElement"/> to read the size updates from</param> + public static void BindSize(this Visual source, UIElement target) + { + var visual = ElementCompositionPreview.GetElementVisual(target); + var bindSizeAnimation = source.Compositor.CreateExpressionAnimation($"{nameof(visual)}.Size"); + + bindSizeAnimation.SetReferenceParameter(nameof(visual), visual); + + // Start the animation + source.StartAnimation("Size", bindSizeAnimation); + } + + /// <summary> + /// Starts an animation on the given property of a <see cref="CompositionObject"/> + /// </summary> + /// <typeparam name="T">The type of the property to animate</typeparam> + /// <param name="target">The target <see cref="CompositionObject"/></param> + /// <param name="property">The name of the property to animate</param> + /// <param name="value">The final value of the property</param> + /// <param name="duration">The animation duration</param> + /// <returns>A <see cref="Task"/> that completes when the created animation completes</returns> + public static Task StartAnimationAsync<T>(this CompositionObject target, string property, T value, TimeSpan duration) + where T : unmanaged + { + // Stop previous animations + target.StopAnimation(property); + + // Setup the animation to run + KeyFrameAnimation animation; + switch (value) + { + case float f: + var scalarAnimation = target.Compositor.CreateScalarKeyFrameAnimation(); + scalarAnimation.InsertKeyFrame(1f, f); + animation = scalarAnimation; + break; + case Color c: + var colorAnimation = target.Compositor.CreateColorKeyFrameAnimation(); + colorAnimation.InsertKeyFrame(1f, c); + animation = colorAnimation; + break; + case Vector4 v4: + var vector4Animation = target.Compositor.CreateVector4KeyFrameAnimation(); + vector4Animation.InsertKeyFrame(1f, v4); + animation = vector4Animation; + break; + default: throw new ArgumentException($"Invalid animation type: {typeof(T)}", nameof(value)); + } + + animation.Duration = duration; + + // Get the batch and start the animations + var batch = target.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation); + + var tcs = new TaskCompletionSource<object?>(); + + batch.Completed += (s, e) => tcs.SetResult(null); + + target.StartAnimation(property, animation); + + batch.End(); + + return tcs.Task; + } +} diff --git a/components/Media/src/Helpers/Cache/CompositionObjectCache{TKey,TValue}.cs b/components/Media/src/Helpers/Cache/CompositionObjectCache{TKey,TValue}.cs new file mode 100644 index 00000000..7846b220 --- /dev/null +++ b/components/Media/src/Helpers/Cache/CompositionObjectCache{TKey,TValue}.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers.Cache; + +/// <summary> +/// A <see langword="class"/> used to cache reusable <see cref="CompositionObject"/> instances with an associated key +/// </summary> +/// <typeparam name="TKey">The type of key to classify the items in the cache</typeparam> +/// <typeparam name="TValue">The type of items stored in the cache</typeparam> +internal sealed class CompositionObjectCache<TKey, TValue> + where TValue : CompositionObject + where TKey : notnull +{ + /// <summary> + /// The cache of weak references of type <typeparamref name="TValue"/> to <typeparamref name="TKey"/> instances, to avoid memory leaks + /// </summary> + private readonly ConditionalWeakTable<Compositor, Dictionary<TKey, WeakReference<TValue>>> cache = new ConditionalWeakTable<Compositor, Dictionary<TKey, WeakReference<TValue>>>(); + + /// <summary> + /// Tries to retrieve a valid instance from the cache, and uses the provided factory if an existing item is not found + /// </summary> + /// <param name="compositor">The current <see cref="Compositor"/> instance to get the value for</param> + /// <param name="key">The key to look for</param> + /// <param name="result">The resulting value, if existing</param> + /// <returns><see langword="true"/> if the target value has been found, <see langword="false"/> otherwise</returns> + public bool TryGetValue(Compositor compositor, TKey key, out TValue? result) + { + lock (this.cache) + { + if (this.cache.TryGetValue(compositor, out var map) && + map.TryGetValue(key, out var reference) && + reference.TryGetTarget(out result)) + { + return true; + } + + result = null; + return false; + } + } + + /// <summary> + /// Adds or updates a value with the specified key to the cache + /// </summary> + /// <param name="compositor">The current <see cref="Compositor"/> instance to get the value for</param> + /// <param name="key">The key of the item to add</param> + /// <param name="value">The value to add</param> + public void AddOrUpdate(Compositor compositor, TKey key, TValue value) + { + lock (this.cache) + { + if (this.cache.TryGetValue(compositor, out var map)) + { + _ = map.Remove(key); + + map.Add(key, new WeakReference<TValue>(value)); + } + else + { + map = new Dictionary<TKey, WeakReference<TValue>> { [key] = new WeakReference<TValue>(value) }; + + this.cache.Add(compositor, map); + } + } + } +} diff --git a/components/Media/src/Helpers/Cache/CompositionObjectCache{T}.cs b/components/Media/src/Helpers/Cache/CompositionObjectCache{T}.cs new file mode 100644 index 00000000..6465b941 --- /dev/null +++ b/components/Media/src/Helpers/Cache/CompositionObjectCache{T}.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers.Cache; + +/// <summary> +/// A <see langword="class"/> used to cache reusable <see cref="CompositionObject"/> instances in each UI thread +/// </summary> +/// <typeparam name="T">The type of instances to cache</typeparam> +internal sealed class CompositionObjectCache<T> + where T : CompositionObject +{ + /// <summary> + /// The cache of weak references of type <typeparamref name="T"/>, to avoid memory leaks + /// </summary> + private readonly ConditionalWeakTable<Compositor, WeakReference<T>> cache = new ConditionalWeakTable<Compositor, WeakReference<T>>(); + + /// <summary> + /// Tries to retrieve a valid <typeparamref name="T"/> instance from the cache, and uses the provided factory if an existing item is not found + /// </summary> + /// <param name="compositor">The current <see cref="Compositor"/> instance to get the value for</param> + /// <param name="producer">A <see cref="Func{TResult}"/> instance used to produce a <typeparamref name="T"/> instance</param> + /// <returns>A <typeparamref name="T"/> instance that is linked to <paramref name="compositor"/></returns> + public T GetValue(Compositor compositor, Func<Compositor, T> producer) + { + lock (cache) + { + if (this.cache.TryGetValue(compositor, out var reference) && + reference.TryGetTarget(out var instance)) + { + return instance; + } + + // Create a new instance when needed + var fallback = producer(compositor); + this.cache.AddOrUpdate(compositor, new WeakReference<T>(fallback)); + + return fallback; + } + } +} diff --git a/components/Media/src/Helpers/SurfaceLoader.Instance.cs b/components/Media/src/Helpers/SurfaceLoader.Instance.cs new file mode 100644 index 00000000..013e4aad --- /dev/null +++ b/components/Media/src/Helpers/SurfaceLoader.Instance.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Text; +using Microsoft.Graphics.Canvas.UI.Composition; +using Windows.UI; + +#if WINUI2 +using Windows.Graphics.DirectX; +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.Graphics.DirectX; +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers; + +/// <summary> +/// A delegate for load time effects. +/// </summary> +/// <param name="bitmap">The bitmap.</param> +/// <param name="device">The device.</param> +/// <param name="sizeTarget">The size target.</param> +/// <returns>A CompositeDrawingSurface</returns> +public delegate CompositionDrawingSurface LoadTimeEffectHandler(CanvasBitmap bitmap, CompositionGraphicsDevice device, Size sizeTarget); + +/// <summary> +/// A <see langword="class"/> that can load and draw images and other objects to Win2D surfaces and brushes +/// </summary> +public sealed partial class SurfaceLoader : IDisposable +{ + /// <summary> + /// The cache of <see cref="SurfaceLoader"/> instances currently available + /// </summary> + private static readonly ConditionalWeakTable<Compositor, SurfaceLoader> Instances = new ConditionalWeakTable<Compositor, SurfaceLoader>(); + + /// <summary> + /// Gets a <see cref="SurfaceLoader"/> instance for the <see cref="Compositor"/> of the current window + /// </summary> + /// <returns>A <see cref="SurfaceLoader"/> instance to use in the current window</returns> + public static SurfaceLoader GetInstance() + { + return GetInstance(Window.Current.Compositor); + } + + /// <summary> + /// Gets a <see cref="SurfaceLoader"/> instance for a given <see cref="Compositor"/> + /// </summary> + /// <param name="compositor">The input <see cref="Compositor"/> object to use</param> + /// <returns>A <see cref="SurfaceLoader"/> instance associated with <paramref name="compositor"/></returns> + public static SurfaceLoader GetInstance(Compositor compositor) + { + lock (Instances) + { + if (Instances.TryGetValue(compositor, out var instance)) + { + return instance; + } + + instance = new SurfaceLoader(compositor); + + Instances.Add(compositor, instance); + + return instance; + } + } + + /// <summary> + /// The <see cref="Compositor"/> instance in use. + /// </summary> + private readonly Compositor compositor; + + /// <summary> + /// The <see cref="CanvasDevice"/> instance in use. + /// </summary> + private CanvasDevice? canvasDevice; + + /// <summary> + /// The <see cref="CompositionGraphicsDevice"/> instance to determine which GPU is handling the request. + /// </summary> + private CompositionGraphicsDevice? compositionDevice; + + /// <summary> + /// Initializes a new instance of the <see cref="SurfaceLoader"/> class. + /// </summary> + /// <param name="compositor">The <see cref="Compositor"/> instance to use</param> + private SurfaceLoader(Compositor compositor) + { + this.compositor = compositor; + + this.InitializeDevices(); + } + + /// <summary> + /// Reloads the <see cref="canvasDevice"/> and <see cref="compositionDevice"/> fields. + /// </summary> + private void InitializeDevices() + { + if (!(this.canvasDevice is null)) + { + this.canvasDevice.DeviceLost -= CanvasDevice_DeviceLost; + } + + if (!(this.compositionDevice is null)) + { + this.compositionDevice.RenderingDeviceReplaced -= CompositionDevice_RenderingDeviceReplaced; + } + + this.canvasDevice = new CanvasDevice(); + this.compositionDevice = CanvasComposition.CreateCompositionGraphicsDevice(this.compositor, this.canvasDevice); + + this.canvasDevice.DeviceLost += CanvasDevice_DeviceLost; + this.compositionDevice.RenderingDeviceReplaced += CompositionDevice_RenderingDeviceReplaced; + } + + /// <summary> + /// Invokes <see cref="InitializeDevices"/> when the current <see cref="CanvasDevice"/> is lost. + /// </summary> + private void CanvasDevice_DeviceLost(CanvasDevice sender, object args) + { + InitializeDevices(); + } + + /// <summary> + /// Invokes <see cref="InitializeDevices"/> when the current <see cref="CompositionGraphicsDevice"/> changes rendering device. + /// </summary> + private void CompositionDevice_RenderingDeviceReplaced(CompositionGraphicsDevice sender, RenderingDeviceReplacedEventArgs args) + { + InitializeDevices(); + } + + /// <summary> + /// Loads an image from the URI. + /// </summary> + /// <param name="uri">The URI.</param> + /// <returns><see cref="CompositionDrawingSurface"/></returns> + public async Task<CompositionDrawingSurface> LoadFromUri(Uri uri) + { + return await LoadFromUri(uri, Size.Empty); + } + + /// <summary> + /// Loads an image from URI with a specified size. + /// </summary> + /// <param name="uri">The URI.</param> + /// <param name="sizeTarget">The size target.</param> + /// <returns><see cref="CompositionDrawingSurface"/></returns> + public async Task<CompositionDrawingSurface> LoadFromUri(Uri uri, Size sizeTarget) + { + default(ArgumentNullException).ThrowIfNull(compositionDevice); + + var bitmap = await CanvasBitmap.LoadAsync(canvasDevice, uri); + var sizeSource = bitmap.Size; + + if (sizeTarget.IsEmpty) + { + sizeTarget = sizeSource; + } + + var surface = compositionDevice.CreateDrawingSurface( + sizeTarget, + DirectXPixelFormat.B8G8R8A8UIntNormalized, + DirectXAlphaMode.Premultiplied); + + using (var ds = CanvasComposition.CreateDrawingSession(surface)) + { + ds.Clear(Color.FromArgb(0, 0, 0, 0)); + ds.DrawImage(bitmap, new Rect(0, 0, sizeTarget.Width, sizeTarget.Height), new Rect(0, 0, sizeSource.Width, sizeSource.Height)); + } + + return surface; + } + + /// <summary> + /// Loads the text on to a <see cref="CompositionDrawingSurface"/>. + /// </summary> + /// <param name="text">The text.</param> + /// <param name="sizeTarget">The size target.</param> + /// <param name="textFormat">The text format.</param> + /// <param name="textColor">Color of the text.</param> + /// <param name="bgColor">Color of the bg.</param> + /// <returns><see cref="CompositionDrawingSurface"/></returns> + public CompositionDrawingSurface LoadText(string text, Size sizeTarget, CanvasTextFormat textFormat, Color textColor, Color bgColor) + { + default(ArgumentNullException).ThrowIfNull(compositionDevice); + + var surface = compositionDevice.CreateDrawingSurface( + sizeTarget, + DirectXPixelFormat.B8G8R8A8UIntNormalized, + DirectXAlphaMode.Premultiplied); + + using (var ds = CanvasComposition.CreateDrawingSession(surface)) + { + ds.Clear(bgColor); + ds.DrawText(text, new Rect(0, 0, sizeTarget.Width, sizeTarget.Height), textColor, textFormat); + } + + return surface; + } + + /// <summary> + /// Loads an image from URI, with a specified size. + /// </summary> + /// <param name="uri">The URI.</param> + /// <param name="sizeTarget">The size target.</param> + /// <param name="loadEffectHandler">The load effect handler callback.</param> + /// <returns><see cref="CompositionDrawingSurface"/></returns> + public async Task<CompositionDrawingSurface> LoadFromUri(Uri uri, Size sizeTarget, LoadTimeEffectHandler loadEffectHandler) + { + default(ArgumentNullException).ThrowIfNull(compositionDevice); + + if (loadEffectHandler != null) + { + var bitmap = await CanvasBitmap.LoadAsync(canvasDevice, uri); + return loadEffectHandler(bitmap, compositionDevice, sizeTarget); + } + + return await LoadFromUri(uri, sizeTarget); + } + + public void Dispose() + { + compositionDevice?.Dispose(); + canvasDevice?.Dispose(); + } +} diff --git a/components/Media/src/Helpers/SurfaceLoader.cs b/components/Media/src/Helpers/SurfaceLoader.cs new file mode 100644 index 00000000..ea40b913 --- /dev/null +++ b/components/Media/src/Helpers/SurfaceLoader.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.UI.Composition; +using CommunityToolkit.WinUI.Media.Helpers.Cache; +using Windows.Graphics.Display; +using Windows.Graphics.Imaging; +using Windows.UI; + +#if WINUI2 +using Windows.UI.Composition; +using Windows.Graphics.DirectX; +#elif WINUI3 +using Microsoft.UI.Composition; +using Microsoft.Graphics.DirectX; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers; + +/// <summary> +/// A <see langword="class"/> that can load and draw images and other objects to Win2D surfaces and brushes +/// </summary> +public sealed partial class SurfaceLoader +{ + /// <summary> + /// Synchronization mutex to access the cache and load Win2D images concurrently + /// </summary> + private static readonly AsyncMutex Win2DMutex = new AsyncMutex(); + + /// <summary> + /// Gets the local cache mapping for previously loaded Win2D images + /// </summary> + private static readonly CompositionObjectCache<Uri, CompositionBrush> Cache = new CompositionObjectCache<Uri, CompositionBrush>(); + + /// <summary> + /// Loads a <see cref="CompositionBrush"/> instance with the target image from the shared <see cref="CanvasDevice"/> instance + /// </summary> + /// <param name="uri">The path to the image to load</param> + /// <param name="dpiMode">Indicates the desired DPI mode to use when loading the image</param> + /// <param name="cacheMode">Indicates the cache option to use to load the image</param> + /// <returns>A <see cref="Task{T}"/> that returns the loaded <see cref="CompositionBrush"/> instance</returns> + public static async Task<CompositionBrush?> LoadImageAsync(Uri uri, DpiMode dpiMode, CacheMode cacheMode = CacheMode.Default) + { + var compositor = Window.Current.Compositor; + + // Lock and check the cache first + using (await Win2DMutex.LockAsync()) + { + uri = uri.ToAppxUri(); + + if (cacheMode == CacheMode.Default && + Cache.TryGetValue(compositor, uri, out var cached)) + { + return cached; + } + + // Load the image + CompositionBrush? brush; + try + { + // This will throw and the canvas will re-initialize the Win2D device if needed + var sharedDevice = CanvasDevice.GetSharedDevice(); + brush = await LoadSurfaceBrushAsync(sharedDevice, compositor, uri, dpiMode); + } + catch + { + // Device error + brush = null; + } + + // Cache when needed and return the result + if (brush != null && + cacheMode != CacheMode.Disabled) + { + Cache.AddOrUpdate(compositor, uri, brush); + } + + return brush; + } + } + + /// <summary> + /// Loads a <see cref="CompositionBrush"/> from the input <see cref="System.Uri"/>, and prepares it to be used in a tile effect + /// </summary> + /// <param name="canvasDevice">The device to use to process the Win2D image</param> + /// <param name="compositor">The compositor instance to use to create the final brush</param> + /// <param name="uri">The path to the image to load</param> + /// <param name="dpiMode">Indicates the desired DPI mode to use when loading the image</param> + /// <returns>A <see cref="Task{T}"/> that returns the loaded <see cref="CompositionBrush"/> instance</returns> + private static async Task<CompositionBrush> LoadSurfaceBrushAsync( + CanvasDevice canvasDevice, + Compositor compositor, + Uri uri, + DpiMode dpiMode) + { + var displayInformation = DisplayInformation.GetForCurrentView(); + float dpi = displayInformation.LogicalDpi; + + // Load the bitmap with the appropriate settings + using CanvasBitmap bitmap = dpiMode switch + { + DpiMode.UseSourceDpi => await CanvasBitmap.LoadAsync(canvasDevice, uri), + DpiMode.Default96Dpi => await CanvasBitmap.LoadAsync(canvasDevice, uri, 96), + DpiMode.DisplayDpi => await CanvasBitmap.LoadAsync(canvasDevice, uri, dpi), + DpiMode.DisplayDpiWith96AsLowerBound => await CanvasBitmap.LoadAsync(canvasDevice, uri, dpi >= 96 ? dpi : 96), + _ => throw new ArgumentOutOfRangeException(nameof(dpiMode), dpiMode, $"Invalid DPI mode: {dpiMode}") + }; + + // Calculate the surface size + Size + size = bitmap.Size, + sizeInPixels = new Size(bitmap.SizeInPixels.Width, bitmap.SizeInPixels.Height); + + // Get the device and the target surface + using CompositionGraphicsDevice graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice); + + // Create the drawing surface + var drawingSurface = graphicsDevice.CreateDrawingSurface( + sizeInPixels, + DirectXPixelFormat.B8G8R8A8UIntNormalized, + DirectXAlphaMode.Premultiplied); + + // Create a drawing session for the target surface + using (var drawingSession = CanvasComposition.CreateDrawingSession(drawingSurface, new Rect(0, 0, sizeInPixels.Width, sizeInPixels.Height), dpi)) + { + // Fill the target surface + drawingSession.Clear(Color.FromArgb(0, 0, 0, 0)); + drawingSession.DrawImage(bitmap, new Rect(0, 0, size.Width, size.Height), new Rect(0, 0, size.Width, size.Height)); + drawingSession.EffectTileSize = new BitmapSize { Width = (uint)size.Width, Height = (uint)size.Height }; + } + + // Setup the effect brush to use + var surfaceBrush = compositor.CreateSurfaceBrush(drawingSurface); + surfaceBrush.Stretch = CompositionStretch.None; + + double pixels = displayInformation.RawPixelsPerViewPixel; + + // Adjust the scale if the DPI scaling is greater than 100% + if (pixels > 1) + { + surfaceBrush.Scale = new Vector2((float)(1 / pixels)); + surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.NearestNeighbor; + } + + return surfaceBrush; + } +} diff --git a/components/Media/src/MultiTarget.props b/components/Media/src/MultiTarget.props new file mode 100644 index 00000000..67f1c274 --- /dev/null +++ b/components/Media/src/MultiTarget.props @@ -0,0 +1,9 @@ +<Project> + <PropertyGroup> + <!-- + MultiTarget is a custom property that indicates which target a project is designed to be built for / run on. + Used to create project references, generate solution files, enable/disable TargetFrameworks, and build nuget packages. + --> + <MultiTarget>uwp;wasdk;</MultiTarget> + </PropertyGroup> +</Project> diff --git a/components/Media/src/Pipelines/BrushProvider.cs b/components/Media/src/Pipelines/BrushProvider.cs new file mode 100644 index 00000000..56572dc2 --- /dev/null +++ b/components/Media/src/Pipelines/BrushProvider.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// <summary> +/// A simple container <see langword="class"/> used to store info on a custom composition effect to create +/// </summary> +public sealed class BrushProvider +{ + /// <summary> + /// Gets the name of the target <see cref="CompositionEffectSourceParameter"/> + /// </summary> + internal string Name { get; } + + /// <summary> + /// Gets the stored effect initializer + /// </summary> + internal Func<ValueTask<CompositionBrush>> Initializer { get; } + + /// <summary> + /// Initializes a new instance of the <see cref="BrushProvider"/> class. + /// </summary> + /// <param name="name">The name of the target <see cref="CompositionEffectSourceParameter"/></param> + /// <param name="initializer">The stored effect initializer</param> + private BrushProvider(string name, Func<ValueTask<CompositionBrush>> initializer) + { + this.Name = name; + this.Initializer = initializer; + } + + /// <summary> + /// Creates a new instance with the info on a given <see cref="CompositionEffectSourceParameter"/> to initialize + /// </summary> + /// <param name="name">The target effect name</param> + /// <param name="brush">A <see cref="CompositionBrush"/> to use to initialize the effect</param> + /// <returns>A <see cref="BrushProvider"/> instance with the input initializer</returns> + [Pure] + public static BrushProvider New(string name, CompositionBrush brush) => new BrushProvider(name, () => new ValueTask<CompositionBrush>(brush)); + + /// <summary> + /// Creates a new instance with the info on a given <see cref="CompositionEffectSourceParameter"/> to initialize + /// </summary> + /// <param name="name">The target effect name</param> + /// <param name="factory">A <see cref="Func{TResult}"/> instance that will produce the <see cref="CompositionBrush"/> to use to initialize the effect</param> + /// <returns>A <see cref="BrushProvider"/> instance with the input initializer</returns> + [Pure] + public static BrushProvider New(string name, Func<CompositionBrush> factory) => new BrushProvider(name, () => new ValueTask<CompositionBrush>(factory())); + + /// <summary> + /// Creates a new instance with the info on a given <see cref="CompositionEffectSourceParameter"/> to initialize + /// </summary> + /// <param name="name">The target effect name</param> + /// <param name="factory">An asynchronous <see cref="Func{TResult}"/> instance that will produce the <see cref="CompositionBrush"/> to use to initialize the effect</param> + /// <returns>A <see cref="BrushProvider"/> instance with the input initializer</returns> + [Pure] + public static BrushProvider New(string name, Func<Task<CompositionBrush>> factory) => new BrushProvider(name, () => new ValueTask<CompositionBrush>(factory())); +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Effects.Internals.cs b/components/Media/src/Pipelines/PipelineBuilder.Effects.Internals.cs new file mode 100644 index 00000000..a93932f1 --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Effects.Internals.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; +using Windows.UI; +using CanvasCrossFadeEffect = Microsoft.Graphics.Canvas.Effects.CrossFadeEffect; +using CanvasExposureEffect = Microsoft.Graphics.Canvas.Effects.ExposureEffect; +using CanvasHueRotationEffect = Microsoft.Graphics.Canvas.Effects.HueRotationEffect; +using CanvasOpacityEffect = Microsoft.Graphics.Canvas.Effects.OpacityEffect; +using CanvasSaturationEffect = Microsoft.Graphics.Canvas.Effects.SaturationEffect; +using CanvasSepiaEffect = Microsoft.Graphics.Canvas.Effects.SepiaEffect; +using CanvasTintEffect = Microsoft.Graphics.Canvas.Effects.TintEffect; + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// <summary> +/// A <see langword="class"/> that allows to build custom effects pipelines and create <see cref="CompositionBrush"/> instances from them +/// </summary> +public sealed partial class PipelineBuilder +{ + /// <summary> + /// Adds a new <see cref="GaussianBlurEffect"/> to the current pipeline + /// </summary> + /// <param name="blur">The blur amount to apply</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <param name="mode">The <see cref="EffectBorderMode"/> parameter for the effect, defaults to <see cref="EffectBorderMode.Hard"/></param> + /// <param name="optimization">The <see cref="EffectOptimization"/> parameter to use, defaults to <see cref="EffectOptimization.Balanced"/></param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + internal PipelineBuilder Blur( + float blur, + out string target, + EffectBorderMode mode = EffectBorderMode.Hard, + EffectOptimization optimization = EffectOptimization.Balanced) + { + string name = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{name}.{nameof(GaussianBlurEffect.BlurAmount)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer(), + Name = name + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// <summary> + /// Cross fades two pipelines using an <see cref="CanvasCrossFadeEffect"/> instance + /// </summary> + /// <param name="pipeline">The second <see cref="PipelineBuilder"/> instance to cross fade</param> + /// <param name="factor">The cross fade factor to blend the input effects (should be in the [0, 1] range)</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(Factory, this, pipeline, new[] { target }); + } + + /// <summary> + /// Applies an exposure effect on the current pipeline + /// </summary> + /// <param name="amount">The initial exposure of tint to apply over the current effect (should be in the [-2, 2] range)</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Exposure(float amount, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasExposureEffect.Exposure)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// <summary> + /// Applies a hue rotation effect on the current pipeline + /// </summary> + /// <param name="angle">The angle to rotate the hue, in radians</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder HueRotation(float angle, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasHueRotationEffect.Angle)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// <summary> + /// Adds a new <see cref="CanvasOpacityEffect"/> to the current pipeline + /// </summary> + /// <param name="opacity">The opacity value to apply to the pipeline</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Opacity(float opacity, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasOpacityEffect.Opacity)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// <summary> + /// Adds a new <see cref="CanvasSaturationEffect"/> to the current pipeline + /// </summary> + /// <param name="saturation">The initial saturation amount for the new effect (should be in the [0, 1] range)</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Saturation(float saturation, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasSaturationEffect.Saturation)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// <summary> + /// Adds a new <see cref="CanvasSepiaEffect"/> to the current pipeline + /// </summary> + /// <param name="intensity">The sepia effect intensity for the new effect</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Sepia(float intensity, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasSepiaEffect.Intensity)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// <summary> + /// Applies a tint effect on the current pipeline + /// </summary> + /// <param name="color">The color to use</param> + /// <param name="target">The target property to animate the resulting effect.</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Tint(Color color, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasTintEffect.Color)}"; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Effects.cs b/components/Media/src/Pipelines/PipelineBuilder.Effects.cs new file mode 100644 index 00000000..a65ca17a --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Effects.cs @@ -0,0 +1,693 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; +using Windows.UI; +using Windows.UI.Composition; +using CanvasExposureEffect = Microsoft.Graphics.Canvas.Effects.ExposureEffect; +using CanvasGrayscaleEffect = Microsoft.Graphics.Canvas.Effects.GrayscaleEffect; +using CanvasHueRotationEffect = Microsoft.Graphics.Canvas.Effects.HueRotationEffect; +using CanvasInvertEffect = Microsoft.Graphics.Canvas.Effects.InvertEffect; +using CanvasLuminanceToAlphaEffect = Microsoft.Graphics.Canvas.Effects.LuminanceToAlphaEffect; +using CanvasOpacityEffect = Microsoft.Graphics.Canvas.Effects.OpacityEffect; +using CanvasSaturationEffect = Microsoft.Graphics.Canvas.Effects.SaturationEffect; +using CanvasSepiaEffect = Microsoft.Graphics.Canvas.Effects.SepiaEffect; +using CanvasTemperatureAndTintEffect = Microsoft.Graphics.Canvas.Effects.TemperatureAndTintEffect; +using CanvasTintEffect = Microsoft.Graphics.Canvas.Effects.TintEffect; + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// <summary> +/// A <see langword="class"/> that allows to build custom effects pipelines and create <see cref="CompositionBrush"/> instances from them +/// </summary> +public sealed partial class PipelineBuilder +{ + /// <summary> + /// Adds a new <see cref="GaussianBlurEffect"/> to the current pipeline + /// </summary> + /// <param name="blur">The blur amount to apply</param> + /// <param name="mode">The <see cref="EffectBorderMode"/> parameter for the effect, defaults to <see cref="EffectBorderMode.Hard"/></param> + /// <param name="optimization">The <see cref="EffectOptimization"/> parameter to use, defaults to <see cref="EffectOptimization.Balanced"/></param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Blur(float blur, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + async ValueTask<IGraphicsEffectSource> Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Adds a new <see cref="GaussianBlurEffect"/> to the current pipeline + /// </summary> + /// <param name="blur">The initial blur amount</param> + /// <param name="setter">The optional blur setter for the effect</param> + /// <param name="mode">The <see cref="EffectBorderMode"/> parameter for the effect, defaults to <see cref="EffectBorderMode.Hard"/></param> + /// <param name="optimization">The <see cref="EffectOptimization"/> parameter to use, defaults to <see cref="EffectOptimization.Balanced"/></param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Blur(float blur, out EffectSetter<float> setter, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(GaussianBlurEffect.BlurAmount)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(GaussianBlurEffect.BlurAmount)}" }); + } + + /// <summary> + /// Adds a new <see cref="GaussianBlurEffect"/> to the current pipeline + /// </summary> + /// <param name="blur">The initial blur amount</param> + /// <param name="animation">The optional blur animation for the effect</param> + /// <param name="mode">The <see cref="EffectBorderMode"/> parameter for the effect, defaults to <see cref="EffectBorderMode.Hard"/></param> + /// <param name="optimization">The <see cref="EffectOptimization"/> parameter to use, defaults to <see cref="EffectOptimization.Balanced"/></param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Blur(float blur, out EffectAnimation<float> animation, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(GaussianBlurEffect.BlurAmount)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(GaussianBlurEffect.BlurAmount)}" }); + } + + /// <summary> + /// Adds a new <see cref="CanvasSaturationEffect"/> to the current pipeline + /// </summary> + /// <param name="saturation">The saturation amount for the new effect (should be in the [0, 1] range)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Saturation(float saturation) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Adds a new <see cref="CanvasSaturationEffect"/> to the current pipeline + /// </summary> + /// <param name="saturation">The initial saturation amount for the new effect (should be in the [0, 1] range)</param> + /// <param name="setter">The optional saturation setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Saturation(float saturation, out EffectSetter<float> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasSaturationEffect.Saturation)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSaturationEffect.Saturation)}" }); + } + + /// <summary> + /// Adds a new <see cref="CanvasSaturationEffect"/> to the current pipeline + /// </summary> + /// <param name="saturation">The initial saturation amount for the new effect (should be in the [0, 1] range)</param> + /// <param name="animation">The optional saturation animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Saturation(float saturation, out EffectAnimation<float> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasSaturationEffect.Saturation)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSaturationEffect.Saturation)}" }); + } + + /// <summary> + /// Adds a new <see cref="CanvasSepiaEffect"/> to the current pipeline + /// </summary> + /// <param name="intensity">The sepia effect intensity for the new effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Sepia(float intensity) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Adds a new <see cref="CanvasSepiaEffect"/> to the current pipeline + /// </summary> + /// <param name="intensity">The sepia effect intensity for the new effect</param> + /// <param name="setter">The optional sepia intensity setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Sepia(float intensity, out EffectSetter<float> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasSepiaEffect.Intensity)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSepiaEffect.Intensity)}" }); + } + + /// <summary> + /// Adds a new <see cref="CanvasSepiaEffect"/> to the current pipeline + /// </summary> + /// <param name="intensity">The sepia effect intensity for the new effect</param> + /// <param name="animation">The sepia intensity animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Sepia(float intensity, out EffectAnimation<float> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasSepiaEffect.Intensity)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSepiaEffect.Intensity)}" }); + } + + /// <summary> + /// Adds a new <see cref="CanvasOpacityEffect"/> to the current pipeline + /// </summary> + /// <param name="opacity">The opacity value to apply to the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Opacity(float opacity) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Adds a new <see cref="CanvasOpacityEffect"/> to the current pipeline + /// </summary> + /// <param name="opacity">The opacity value to apply to the pipeline</param> + /// <param name="setter">The optional opacity setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Opacity(float opacity, out EffectSetter<float> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasOpacityEffect.Opacity)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasOpacityEffect.Opacity)}" }); + } + + /// <summary> + /// Adds a new <see cref="CanvasOpacityEffect"/> to the current pipeline + /// </summary> + /// <param name="opacity">The opacity value to apply to the pipeline</param> + /// <param name="animation">The optional opacity animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Opacity(float opacity, out EffectAnimation<float> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasOpacityEffect.Opacity)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasOpacityEffect.Opacity)}" }); + } + + /// <summary> + /// Applies an exposure effect on the current pipeline + /// </summary> + /// <param name="amount">The amount of exposure to apply over the current effect (should be in the [-2, 2] range)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Exposure(float amount) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Applies an exposure effect on the current pipeline + /// </summary> + /// <param name="amount">The initial exposure of tint to apply over the current effect (should be in the [-2, 2] range)</param> + /// <param name="setter">The optional amount setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Exposure(float amount, out EffectSetter<float> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasExposureEffect.Exposure)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasExposureEffect.Exposure)}" }); + } + + /// <summary> + /// Applies an exposure effect on the current pipeline + /// </summary> + /// <param name="amount">The initial exposure of tint to apply over the current effect (should be in the [-2, 2] range)</param> + /// <param name="animation">The optional amount animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Exposure(float amount, out EffectAnimation<float> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasExposureEffect.Exposure)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasExposureEffect.Exposure)}" }); + } + + /// <summary> + /// Applies a hue rotation effect on the current pipeline + /// </summary> + /// <param name="angle">The angle to rotate the hue, in radians</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder HueRotation(float angle) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Applies a hue rotation effect on the current pipeline + /// </summary> + /// <param name="angle">The angle to rotate the hue, in radians</param> + /// <param name="setter">The optional rotation angle setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder HueRotation(float angle, out EffectSetter<float> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasHueRotationEffect.Angle)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasHueRotationEffect.Angle)}" }); + } + + /// <summary> + /// Applies a hue rotation effect on the current pipeline + /// </summary> + /// <param name="angle">The angle to rotate the hue, in radians</param> + /// <param name="animation">The optional rotation angle animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder HueRotation(float angle, out EffectAnimation<float> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasHueRotationEffect.Angle)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasHueRotationEffect.Angle)}" }); + } + + /// <summary> + /// Applies a tint effect on the current pipeline + /// </summary> + /// <param name="color">The color to use</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Tint(Color color) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Applies a tint effect on the current pipeline + /// </summary> + /// <param name="color">The color to use</param> + /// <param name="setter">The optional color setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Tint(Color color, out EffectSetter<Color> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertColor($"{id}.{nameof(CanvasTintEffect.Color)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTintEffect.Color)}" }); + } + + /// <summary> + /// Applies a tint effect on the current pipeline + /// </summary> + /// <param name="color">The color to use</param> + /// <param name="animation">The optional color animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Tint(Color color, out EffectAnimation<Color> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasTintEffect.Color)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTintEffect.Color)}" }); + } + + /// <summary> + /// Applies a temperature and tint effect on the current pipeline + /// </summary> + /// <param name="temperature">The temperature value to use (should be in the [-1, 1] range)</param> + /// <param name="tint">The tint value to use (should be in the [-1, 1] range)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder TemperatureAndTint(float temperature, float tint) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasTemperatureAndTintEffect + { + Temperature = temperature, + Tint = tint, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Applies a temperature and tint effect on the current pipeline + /// </summary> + /// <param name="temperature">The temperature value to use (should be in the [-1, 1] range)</param> + /// <param name="temperatureSetter">The optional temperature setter for the effect</param> + /// <param name="tint">The tint value to use (should be in the [-1, 1] range)</param> + /// <param name="tintSetter">The optional tint setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder TemperatureAndTint( + float temperature, + out EffectSetter<float> temperatureSetter, + float tint, + out EffectSetter<float> tintSetter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasTemperatureAndTintEffect + { + Temperature = temperature, + Tint = tint, + Source = await this.sourceProducer(), + Name = id + }; + + temperatureSetter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", value); + + tintSetter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", $"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}" }); + } + + /// <summary> + /// Applies a temperature and tint effect on the current pipeline + /// </summary> + /// <param name="temperature">The temperature value to use (should be in the [-1, 1] range)</param> + /// <param name="temperatureAnimation">The optional temperature animation for the effect</param> + /// <param name="tint">The tint value to use (should be in the [-1, 1] range)</param> + /// <param name="tintAnimation">The optional tint animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder TemperatureAndTint( + float temperature, + out EffectAnimation<float> temperatureAnimation, + float tint, + out EffectAnimation<float> tintAnimation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasTemperatureAndTintEffect + { + Temperature = temperature, + Tint = tint, + Source = await this.sourceProducer(), + Name = id + }; + + temperatureAnimation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", value, duration); + + tintAnimation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", $"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}" }); + } + + /// <summary> + /// Applies a shade effect on the current pipeline + /// </summary> + /// <param name="color">The color to use</param> + /// <param name="mix">The amount of mix to apply over the current effect (must be in the [0, 1] range)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Shade(Color color, float mix) + { + return FromColor(color).CrossFade(this, mix); + } + + /// <summary> + /// Applies a shade effect on the current pipeline + /// </summary> + /// <param name="color">The color to use</param> + /// <param name="colorSetter">The optional color setter for the effect</param> + /// <param name="mix">The initial amount of mix to apply over the current effect (must be in the [0, 1] range)</param> + /// <param name="mixSetter">The optional mix setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Shade( + Color color, + out EffectSetter<Color> colorSetter, + float mix, + out EffectSetter<float> mixSetter) + { + return FromColor(color, out colorSetter).CrossFade(this, mix, out mixSetter); + } + + /// <summary> + /// Applies a shade effect on the current pipeline + /// </summary> + /// <param name="color">The color to use</param> + /// <param name="colorAnimation">The optional color animation for the effect</param> + /// <param name="mix">The initial amount of mix to apply over the current effect (must be in the [0, 1] range)</param> + /// <param name="mixAnimation">The optional mix animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Shade( + Color color, + out EffectAnimation<Color> colorAnimation, + float mix, + out EffectAnimation<float> mixAnimation) + { + return FromColor(color, out colorAnimation).CrossFade(this, mix, out mixAnimation); + } + + /// <summary> + /// Applies a luminance to alpha effect on the current pipeline + /// </summary> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder LuminanceToAlpha() + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasLuminanceToAlphaEffect + { + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Applies an invert effect on the current pipeline + /// </summary> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Invert() + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasInvertEffect + { + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Applies a grayscale on the current pipeline + /// </summary> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Grayscale() + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasGrayscaleEffect + { + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// <summary> + /// Applies a custom effect to the current pipeline + /// </summary> + /// <param name="factory">A <see cref="Func{T, TResult}"/> that takes the current <see cref="IGraphicsEffectSource"/> instance and produces a new effect to display</param> + /// <param name="animations">The list of optional animatable properties in the returned effect</param> + /// <param name="initializers">The list of source parameters that require deferred initialization (see <see cref="CompositionEffectSourceParameter"/> for more info)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Effect( + Func<IGraphicsEffectSource, IGraphicsEffectSource> factory, + IEnumerable<string>? animations = null, + IEnumerable<BrushProvider>? initializers = null) + { + async ValueTask<IGraphicsEffectSource> Factory() => factory(await this.sourceProducer()); + + return new PipelineBuilder(this, Factory, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + /// <summary> + /// Applies a custom effect to the current pipeline + /// </summary> + /// <param name="factory">An asynchronous <see cref="Func{T, TResult}"/> that takes the current <see cref="IGraphicsEffectSource"/> instance and produces a new effect to display</param> + /// <param name="animations">The list of optional animatable properties in the returned effect</param> + /// <param name="initializers">The list of source parameters that require deferred initialization (see <see cref="CompositionEffectSourceParameter"/> for more info)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Effect( + Func<IGraphicsEffectSource, Task<IGraphicsEffectSource>> factory, + IEnumerable<string>? animations = null, + IEnumerable<BrushProvider>? initializers = null) + { + async ValueTask<IGraphicsEffectSource> Factory() => await factory(await this.sourceProducer()); + + return new PipelineBuilder(this, Factory, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Initialization.cs b/components/Media/src/Pipelines/PipelineBuilder.Initialization.cs new file mode 100644 index 00000000..8997d397 --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Initialization.cs @@ -0,0 +1,327 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using System.Numerics; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using CommunityToolkit.WinUI.Media.Helpers; +using CommunityToolkit.WinUI.Media.Helpers.Cache; +using Windows.Graphics.Effects; +using Windows.UI; + +#if WINUI3 +using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Xaml.Hosting; +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// <summary> +/// A <see langword="class"/> that allows to build custom effects pipelines and create <see cref="CompositionBrush"/> instances from them +/// </summary> +public sealed partial class PipelineBuilder +{ + /// <summary> + /// The cache manager for backdrop brushes + /// </summary> + private static readonly CompositionObjectCache<CompositionBrush> BackdropBrushCache = new CompositionObjectCache<CompositionBrush>(); + + /// <summary> + /// The cache manager for host backdrop brushes + /// </summary> + private static readonly CompositionObjectCache<CompositionBrush> HostBackdropBrushCache = new CompositionObjectCache<CompositionBrush>(); + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the <see cref="CompositionBrush"/> returned by <see cref="Compositor.CreateBackdropBrush"/> + /// </summary> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromBackdrop() + { + ValueTask<CompositionBrush> Factory() + { + var brush = BackdropBrushCache.GetValue(Window.Current.Compositor, c => c.CreateBackdropBrush()); + + return new ValueTask<CompositionBrush>(brush); + } + + return new PipelineBuilder(Factory); + } + +#if WINUI2 + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the <see cref="CompositionBrush"/> returned by <see cref="Compositor.CreateHostBackdropBrush"/> + /// </summary> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromHostBackdrop() + { + ValueTask<CompositionBrush> Factory() + { + var brush = HostBackdropBrushCache.GetValue(Window.Current.Compositor, c => c.CreateHostBackdropBrush()); + + return new ValueTask<CompositionBrush>(brush); + } + + return new PipelineBuilder(Factory); + } +#endif + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a solid <see cref="CompositionBrush"/> with the specified color + /// </summary> + /// <param name="color">The desired color for the initial <see cref="CompositionBrush"/></param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromColor(Color color) + { + return new PipelineBuilder(() => new ValueTask<IGraphicsEffectSource>(new ColorSourceEffect { Color = color })); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a solid <see cref="CompositionBrush"/> with the specified color + /// </summary> + /// <param name="color">The desired color for the initial <see cref="CompositionBrush"/></param> + /// <param name="setter">The optional color setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromColor(Color color, out EffectSetter<Color> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask<IGraphicsEffectSource> Factory() => new ValueTask<IGraphicsEffectSource>(new ColorSourceEffect + { + Color = color, + Name = id + }); + + setter = (brush, value) => brush.Properties.InsertColor($"{id}.{nameof(ColorSourceEffect.Color)}", value); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.Color)}" }); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a solid <see cref="CompositionBrush"/> with the specified color + /// </summary> + /// <param name="color">The desired color for the initial <see cref="CompositionBrush"/></param> + /// <param name="animation">The optional color animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromColor(Color color, out EffectAnimation<Color> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask<IGraphicsEffectSource> Factory() => new ValueTask<IGraphicsEffectSource>(new ColorSourceEffect + { + Color = color, + Name = id + }); + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(ColorSourceEffect.Color)}", value, duration); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.Color)}" }); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a solid <see cref="CompositionBrush"/> with the specified color + /// </summary> + /// <param name="color">The desired color for the initial <see cref="CompositionBrush"/></param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromHdrColor(Vector4 color) + { + return new PipelineBuilder(() => new ValueTask<IGraphicsEffectSource>(new ColorSourceEffect { ColorHdr = color })); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a solid <see cref="CompositionBrush"/> with the specified color + /// </summary> + /// <param name="color">The desired color for the initial <see cref="CompositionBrush"/></param> + /// <param name="setter">The optional color setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromHdrColor(Vector4 color, out EffectSetter<Vector4> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask<IGraphicsEffectSource> Factory() => new ValueTask<IGraphicsEffectSource>(new ColorSourceEffect + { + ColorHdr = color, + Name = id + }); + + setter = (brush, value) => brush.Properties.InsertVector4($"{id}.{nameof(ColorSourceEffect.ColorHdr)}", value); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.ColorHdr)}" }); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a solid <see cref="CompositionBrush"/> with the specified color + /// </summary> + /// <param name="color">The desired color for the initial <see cref="CompositionBrush"/></param> + /// <param name="animation">The optional color animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromHdrColor(Vector4 color, out EffectAnimation<Vector4> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask<IGraphicsEffectSource> Factory() => new ValueTask<IGraphicsEffectSource>(new ColorSourceEffect + { + ColorHdr = color, + Name = id + }); + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(ColorSourceEffect.ColorHdr)}", value, duration); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.ColorHdr)}" }); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the input <see cref="CompositionBrush"/> instance + /// </summary> + /// <param name="brush">A <see cref="CompositionBrush"/> instance to start the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromBrush(CompositionBrush brush) + { + return new PipelineBuilder(() => new ValueTask<CompositionBrush>(brush)); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the input <see cref="CompositionBrush"/> instance + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> that synchronously returns a <see cref="CompositionBrush"/> instance to start the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromBrush(Func<CompositionBrush> factory) + { + return new PipelineBuilder(() => new ValueTask<CompositionBrush>(factory())); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the input <see cref="CompositionBrush"/> instance + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> that asynchronously returns a <see cref="CompositionBrush"/> instance to start the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromBrush(Func<Task<CompositionBrush>> factory) + { + async ValueTask<CompositionBrush> Factory() => await factory(); + + return new PipelineBuilder(Factory); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the input <see cref="IGraphicsEffectSource"/> instance + /// </summary> + /// <param name="effect">A <see cref="IGraphicsEffectSource"/> instance to start the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromEffect(IGraphicsEffectSource effect) + { + return new PipelineBuilder(() => new ValueTask<IGraphicsEffectSource>(effect)); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the input <see cref="IGraphicsEffectSource"/> instance + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> that synchronously returns a <see cref="IGraphicsEffectSource"/> instance to start the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromEffect(Func<IGraphicsEffectSource> factory) + { + return new PipelineBuilder(() => new ValueTask<IGraphicsEffectSource>(factory())); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the input <see cref="IGraphicsEffectSource"/> instance + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> that asynchronously returns a <see cref="IGraphicsEffectSource"/> instance to start the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromEffect(Func<Task<IGraphicsEffectSource>> factory) + { + async ValueTask<IGraphicsEffectSource> Factory() => await factory(); + + return new PipelineBuilder(Factory); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a Win2D image + /// </summary> + /// <param name="relativePath">The relative path for the image to load (eg. "/Assets/image.png")</param> + /// <param name="dpiMode">Indicates the desired DPI mode to use when loading the image</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromImage(string relativePath, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + return FromImage(relativePath.ToAppxUri(), dpiMode, cacheMode); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a Win2D image + /// </summary> + /// <param name="uri">The path for the image to load</param> + /// <param name="dpiMode">Indicates the desired DPI mode to use when loading the image</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromImage(Uri uri, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + return new PipelineBuilder(() => new ValueTask<CompositionBrush>(SurfaceLoader.LoadImageAsync(uri, dpiMode, cacheMode)!)); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a Win2D image tiled to cover the available space + /// </summary> + /// <param name="relativePath">The relative path for the image to load (eg. "/Assets/image.png")</param> + /// <param name="dpiMode">Indicates the desired DPI mode to use when loading the image</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromTiles(string relativePath, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + return FromTiles(relativePath.ToAppxUri(), dpiMode, cacheMode); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from a Win2D image tiled to cover the available space + /// </summary> + /// <param name="uri">The path for the image to load</param> + /// <param name="dpiMode">Indicates the desired DPI mode to use when loading the image</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromTiles(Uri uri, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + var image = FromImage(uri, dpiMode, cacheMode); + + async ValueTask<IGraphicsEffectSource> Factory() => new BorderEffect + { + ExtendX = CanvasEdgeBehavior.Wrap, + ExtendY = CanvasEdgeBehavior.Wrap, + Source = await image.sourceProducer() + }; + + return new PipelineBuilder(image, Factory); + } + + /// <summary> + /// Starts a new <see cref="PipelineBuilder"/> pipeline from the <see cref="CompositionBrush"/> returned by <see cref="Compositor.CreateBackdropBrush"/> on the input <see cref="UIElement"/> + /// </summary> + /// <param name="element">The source <see cref="UIElement"/> to use to create the pipeline</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromUIElement(UIElement element) + { + return new PipelineBuilder(() => new ValueTask<CompositionBrush>(ElementCompositionPreview.GetElementVisual(element).Compositor.CreateBackdropBrush())); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Merge.cs b/components/Media/src/Pipelines/PipelineBuilder.Merge.cs new file mode 100644 index 00000000..5fa5db08 --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Merge.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; +using CanvasBlendEffect = Microsoft.Graphics.Canvas.Effects.BlendEffect; +using CanvasCrossFadeEffect = Microsoft.Graphics.Canvas.Effects.CrossFadeEffect; + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// <summary> +/// A <see langword="class"/> that allows to build custom effects pipelines and create <see cref="CompositionBrush"/> instances from them +/// </summary> +public sealed partial class PipelineBuilder +{ + /// <summary> + /// Blends two pipelines using a <see cref="BlendEffect"/> instance with the specified mode + /// </summary> + /// <param name="pipeline">The second <see cref="PipelineBuilder"/> instance to blend</param> + /// <param name="mode">The desired <see cref="BlendEffectMode"/> to use to blend the input pipelines</param> + /// <param name="placement">The placemeht to use with the two input pipelines (the default is <see cref="Placement.Foreground"/>)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Blend(PipelineBuilder pipeline, BlendEffectMode mode, Placement placement = Placement.Foreground) + { + var (foreground, background) = placement switch + { + Placement.Foreground => (pipeline, this), + Placement.Background => (this, pipeline), + _ => throw new ArgumentException($"Invalid placement value: {placement}") + }; + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasBlendEffect + { + Foreground = await foreground.sourceProducer(), + Background = await background.sourceProducer(), + Mode = mode + }; + + return new PipelineBuilder(Factory, foreground, background); + } + + /// <summary> + /// Cross fades two pipelines using an <see cref="CanvasCrossFadeEffect"/> instance + /// </summary> + /// <param name="pipeline">The second <see cref="PipelineBuilder"/> instance to cross fade</param> + /// <param name="factor">The cross fade factor to blend the input effects (default is 0.5, must be in the [0, 1] range)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor = 0.5f) + { + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer() + }; + + return new PipelineBuilder(Factory, this, pipeline); + } + + /// <summary> + /// Cross fades two pipelines using an <see cref="CanvasCrossFadeEffect"/> instance + /// </summary> + /// <param name="pipeline">The second <see cref="PipelineBuilder"/> instance to cross fade</param> + /// <param name="factor">The cross fade factor to blend the input effects (should be in the [0, 1] range)</param> + /// <param name="setter">The optional blur setter for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor, out EffectSetter<float> setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}", value); + + return new PipelineBuilder(Factory, this, pipeline, new[] { $"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}" }); + } + + /// <summary> + /// Cross fades two pipelines using an <see cref="CanvasCrossFadeEffect"/> instance + /// </summary> + /// <param name="pipeline">The second <see cref="PipelineBuilder"/> instance to cross fade</param> + /// <param name="factor">The cross fade factor to blend the input effects (should be in the [0, 1] range)</param> + /// <param name="animation">The optional blur animation for the effect</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor, out EffectAnimation<float> animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask<IGraphicsEffectSource> Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}", value, duration); + + return new PipelineBuilder(Factory, this, pipeline, new[] { $"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}" }); + } + + /// <summary> + /// Blends two pipelines using the provided <see cref="Func{T1, T2, TResult}"/> to do so + /// </summary> + /// <param name="factory">The blend function to use</param> + /// <param name="background">The background pipeline to blend with the current instance</param> + /// <param name="animations">The list of optional animatable properties in the returned effect</param> + /// <param name="initializers">The list of source parameters that require deferred initialization (see <see cref="CompositionEffectSourceParameter"/> for more info)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Merge( + Func<IGraphicsEffectSource, IGraphicsEffectSource, IGraphicsEffectSource> factory, + PipelineBuilder background, + IEnumerable<string>? animations = null, + IEnumerable<BrushProvider>? initializers = null) + { + async ValueTask<IGraphicsEffectSource> Factory() => factory(await this.sourceProducer(), await background.sourceProducer()); + + return new PipelineBuilder(Factory, this, background, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + /// <summary> + /// Blends two pipelines using the provided asynchronous <see cref="Func{T1, T2, TResult}"/> to do so + /// </summary> + /// <param name="factory">The asynchronous blend function to use</param> + /// <param name="background">The background pipeline to blend with the current instance</param> + /// <param name="animations">The list of optional animatable properties in the returned effect</param> + /// <param name="initializers">The list of source parameters that require deferred initialization (see <see cref="CompositionEffectSourceParameter"/> for more info)</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public PipelineBuilder Merge( + Func<IGraphicsEffectSource, IGraphicsEffectSource, Task<IGraphicsEffectSource>> factory, + PipelineBuilder background, + IEnumerable<string>? animations = null, + IEnumerable<BrushProvider>? initializers = null) + { + async ValueTask<IGraphicsEffectSource> Factory() => await factory(await this.sourceProducer(), await background.sourceProducer()); + + return new PipelineBuilder(Factory, this, background, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Prebuilt.cs b/components/Media/src/Pipelines/PipelineBuilder.Prebuilt.cs new file mode 100644 index 00000000..bb95863e --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Prebuilt.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// <summary> +/// A <see langword="class"/> that allows to build custom effects pipelines and create <see cref="Windows.UI.Composition.CompositionBrush"/> instances from them +/// </summary> +public sealed partial class PipelineBuilder +{ +#if WINUI2 + /// <summary> + /// Returns a new <see cref="PipelineBuilder"/> instance that implements the host backdrop acrylic effect + /// </summary> + /// <param name="tintColor">The tint color to use</param> + /// <param name="tintOpacity">The amount of tint to apply over the current effect</param> + /// <param name="noiseUri">The <see cref="Uri"/> for the noise texture to load for the acrylic effect</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromHostBackdropAcrylic( + Color tintColor, + float tintOpacity, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromHostBackdrop() + .LuminanceToAlpha() + .Opacity(0.4f) + .Blend(FromHostBackdrop(), BlendEffectMode.Multiply) + .Shade(tintColor, tintOpacity); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// <summary> + /// Returns a new <see cref="PipelineBuilder"/> instance that implements the host backdrop acrylic effect + /// </summary> + /// <param name="tintColor">The tint color to use</param> + /// <param name="tintColorSetter">The optional tint color setter for the effect</param> + /// <param name="tintOpacity">The amount of tint to apply over the current effect</param> + /// <param name="tintOpacitySetter">The optional tint mix setter for the effect</param> + /// <param name="noiseUri">The <see cref="Uri"/> for the noise texture to load for the acrylic effect</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromHostBackdropAcrylic( + Color tintColor, + out EffectSetter<Color> tintColorSetter, + float tintOpacity, + out EffectSetter<float> tintOpacitySetter, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromHostBackdrop() + .LuminanceToAlpha() + .Opacity(0.4f) + .Blend(FromHostBackdrop(), BlendEffectMode.Multiply) + .Shade(tintColor, out tintColorSetter, tintOpacity, out tintOpacitySetter); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// <summary> + /// Returns a new <see cref="PipelineBuilder"/> instance that implements the host backdrop acrylic effect + /// </summary> + /// <param name="tintColor">The tint color to use</param> + /// <param name="tintColorAnimation">The optional tint color animation for the effect</param> + /// <param name="tintOpacity">The amount of tint to apply over the current effect</param> + /// <param name="tintOpacityAnimation">The optional tint mix animation for the effect</param> + /// <param name="noiseUri">The <see cref="Uri"/> for the noise texture to load for the acrylic effect</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromHostBackdropAcrylic( + Color tintColor, + out EffectAnimation<Color> tintColorAnimation, + float tintOpacity, + out EffectAnimation<float> tintOpacityAnimation, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromHostBackdrop() + .LuminanceToAlpha() + .Opacity(0.4f) + .Blend(FromHostBackdrop(), BlendEffectMode.Multiply) + .Shade(tintColor, out tintColorAnimation, tintOpacity, out tintOpacityAnimation); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } +#endif + + /// <summary> + /// Returns a new <see cref="PipelineBuilder"/> instance that implements the in-app backdrop acrylic effect + /// </summary> + /// <param name="tintColor">The tint color to use</param> + /// <param name="tintOpacity">The amount of tint to apply over the current effect (must be in the [0, 1] range)</param> + /// <param name="blurAmount">The amount of blur to apply to the acrylic brush</param> + /// <param name="noiseUri">The <see cref="Uri"/> for the noise texture to load for the acrylic effect</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromBackdropAcrylic( + Color tintColor, + float tintOpacity, + float blurAmount, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = FromBackdrop().Shade(tintColor, tintOpacity).Blur(blurAmount); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// <summary> + /// Returns a new <see cref="PipelineBuilder"/> instance that implements the in-app backdrop acrylic effect + /// </summary> + /// <param name="tintColor">The tint color to use</param> + /// <param name="tintColorSetter">The optional tint color setter for the effect</param> + /// <param name="tintOpacity">The amount of tint to apply over the current effect</param> + /// <param name="tintOpacitySetter">The optional tint mix setter for the effect</param> + /// <param name="blurAmount">The amount of blur to apply to the acrylic brush</param> + /// <param name="blurAmountSetter">The optional blur setter for the effect</param> + /// <param name="noiseUri">The <see cref="Uri"/> for the noise texture to load for the acrylic effect</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromBackdropAcrylic( + Color tintColor, + out EffectSetter<Color> tintColorSetter, + float tintOpacity, + out EffectSetter<float> tintOpacitySetter, + float blurAmount, + out EffectSetter<float> blurAmountSetter, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromBackdrop() + .Shade(tintColor, out tintColorSetter, tintOpacity, out tintOpacitySetter) + .Blur(blurAmount, out blurAmountSetter); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// <summary> + /// Returns a new <see cref="PipelineBuilder"/> instance that implements the in-app backdrop acrylic effect + /// </summary> + /// <param name="tintColor">The tint color to use</param> + /// <param name="tintAnimation">The optional tint color animation for the effect</param> + /// <param name="tintOpacity">The amount of tint to apply over the current effect</param> + /// <param name="tintOpacityAnimation">The optional tint mix animation for the effect</param> + /// <param name="blurAmount">The amount of blur to apply to the acrylic brush</param> + /// <param name="blurAmountAnimation">The optional blur animation for the effect</param> + /// <param name="noiseUri">The <see cref="Uri"/> for the noise texture to load for the acrylic effect</param> + /// <param name="cacheMode">The cache mode to use to load the image</param> + /// <returns>A new <see cref="PipelineBuilder"/> instance to use to keep adding new effects</returns> + [Pure] + public static PipelineBuilder FromBackdropAcrylic( + Color tintColor, + out EffectAnimation<Color> tintAnimation, + float tintOpacity, + out EffectAnimation<float> tintOpacityAnimation, + float blurAmount, + out EffectAnimation<float> blurAmountAnimation, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromBackdrop() + .Shade(tintColor, out tintAnimation, tintOpacity, out tintOpacityAnimation) + .Blur(blurAmount, out blurAmountAnimation); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.cs b/components/Media/src/Pipelines/PipelineBuilder.cs new file mode 100644 index 00000000..4057c7ea --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using System.Numerics; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; + +#if WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#elif WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// <summary> +/// A <see langword="delegate"/> that represents a custom effect property setter that can be applied to a <see cref="CompositionBrush"/> +/// </summary> +/// <typeparam name="T">The type of property value to set</typeparam> +/// <param name="brush">The target <see cref="CompositionBrush"/> instance to target</param> +/// <param name="value">The property value to set</param> +public delegate void EffectSetter<in T>(CompositionBrush brush, T value) + where T : unmanaged; + +/// <summary> +/// A <see langword="delegate"/> that represents a custom effect property animation that can be applied to a <see cref="CompositionBrush"/> +/// </summary> +/// <typeparam name="T">The type of property value to animate</typeparam> +/// <param name="brush">The target <see cref="CompositionBrush"/> instance to use to start the animation</param> +/// <param name="value">The animation target value</param> +/// <param name="duration">The animation duration</param> +/// <returns>A <see cref="Task"/> that completes when the target animation completes</returns> +public delegate Task EffectAnimation<in T>(CompositionBrush brush, T value, TimeSpan duration) + where T : unmanaged; + +/// <summary> +/// A <see langword="class"/> that allows to build custom effects pipelines and create <see cref="CompositionBrush"/> instances from them +/// </summary> +public sealed partial class PipelineBuilder +{ + /// <summary> + /// The <see cref="Func{TResult}"/> instance used to produce the output <see cref="IGraphicsEffectSource"/> for this pipeline + /// </summary> + private readonly Func<ValueTask<IGraphicsEffectSource>> sourceProducer; + + /// <summary> + /// The collection of animation properties present in the current pipeline + /// </summary> + private readonly IReadOnlyCollection<string> animationProperties; + + /// <summary> + /// The collection of info on the parameters that need to be initialized after creating the final <see cref="CompositionBrush"/> + /// </summary> + private readonly IReadOnlyDictionary<string, Func<ValueTask<CompositionBrush>>> lazyParameters; + + /// <summary> + /// Initializes a new instance of the <see cref="PipelineBuilder"/> class. + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> instance that will return the initial <see cref="CompositionBrush"/></param> + private PipelineBuilder(Func<ValueTask<CompositionBrush>> factory) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + this.sourceProducer = () => new ValueTask<IGraphicsEffectSource>(new CompositionEffectSourceParameter(id)); + this.animationProperties = Array.Empty<string>(); + this.lazyParameters = new Dictionary<string, Func<ValueTask<CompositionBrush>>> { { id, factory } }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="PipelineBuilder"/> class. + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> instance that will return the initial <see cref="IGraphicsEffectSource"/></param> + private PipelineBuilder(Func<ValueTask<IGraphicsEffectSource>> factory) + : this( + factory, + Array.Empty<string>(), + new Dictionary<string, Func<ValueTask<CompositionBrush>>>()) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="PipelineBuilder"/> class. + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> instance that will produce the new <see cref="IGraphicsEffectSource"/> to add to the pipeline</param> + /// <param name="animations">The collection of animation properties for the new effect</param> + private PipelineBuilder( + Func<ValueTask<IGraphicsEffectSource>> factory, + IReadOnlyCollection<string> animations) + : this( + factory, + animations, + new Dictionary<string, Func<ValueTask<CompositionBrush>>>()) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="PipelineBuilder"/> class. + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> instance that will produce the new <see cref="IGraphicsEffectSource"/> to add to the pipeline</param> + /// <param name="animations">The collection of animation properties for the new effect</param> + /// <param name="lazy">The collection of <see cref="CompositionBrush"/> instances that needs to be initialized for the new effect</param> + private PipelineBuilder( + Func<ValueTask<IGraphicsEffectSource>> factory, + IReadOnlyCollection<string> animations, + IReadOnlyDictionary<string, Func<ValueTask<CompositionBrush>>> lazy) + { + this.sourceProducer = factory; + this.animationProperties = animations; + this.lazyParameters = lazy; + } + + /// <summary> + /// Initializes a new instance of the <see cref="PipelineBuilder"/> class. + /// </summary> + /// <param name="source">The source pipeline to attach the new effect to</param> + /// <param name="factory">A <see cref="Func{TResult}"/> instance that will produce the new <see cref="IGraphicsEffectSource"/> to add to the pipeline</param> + /// <param name="animations">The collection of animation properties for the new effect</param> + /// <param name="lazy">The collection of <see cref="CompositionBrush"/> instances that needs to be initialized for the new effect</param> + private PipelineBuilder( + PipelineBuilder source, + Func<ValueTask<IGraphicsEffectSource>> factory, + IReadOnlyCollection<string>? animations = null, + IReadOnlyDictionary<string, Func<ValueTask<CompositionBrush>>>? lazy = null) + : this( + factory, + animations?.Merge(source.animationProperties) ?? source.animationProperties, + lazy?.Merge(source.lazyParameters) ?? source.lazyParameters) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="PipelineBuilder"/> class. + /// </summary> + /// <param name="factory">A <see cref="Func{TResult}"/> instance that will produce the new <see cref="IGraphicsEffectSource"/> to add to the pipeline</param> + /// <param name="a">The first pipeline to merge</param> + /// <param name="b">The second pipeline to merge</param> + /// <param name="animations">The collection of animation properties for the new effect</param> + /// <param name="lazy">The collection of <see cref="CompositionBrush"/> instances that needs to be initialized for the new effect</param> + private PipelineBuilder( + Func<ValueTask<IGraphicsEffectSource>> factory, + PipelineBuilder a, + PipelineBuilder b, + IReadOnlyCollection<string>? animations = null, + IReadOnlyDictionary<string, Func<ValueTask<CompositionBrush>>>? lazy = null) + : this( + factory, + animations?.Merge(a.animationProperties.Merge(b.animationProperties)) ?? a.animationProperties.Merge(b.animationProperties), + lazy?.Merge(a.lazyParameters.Merge(b.lazyParameters)) ?? a.lazyParameters.Merge(b.lazyParameters)) + { + } + + /// <summary> + /// Builds a <see cref="CompositionBrush"/> instance from the current effects pipeline + /// </summary> + /// <returns>A <see cref="Task{T}"/> that returns the final <see cref="CompositionBrush"/> instance to use</returns> + [Pure] + public async Task<CompositionBrush> BuildAsync() + { + var effect = await this.sourceProducer() as IGraphicsEffect; + + // Validate the pipeline + if (effect is null) + { + throw new InvalidOperationException("The pipeline doesn't contain a valid effects sequence"); + } + + // Build the effects factory + var factory = this.animationProperties.Count > 0 + ? Window.Current.Compositor.CreateEffectFactory(effect, this.animationProperties) + : Window.Current.Compositor.CreateEffectFactory(effect); + + // Create the effect factory and apply the final effect + var effectBrush = factory.CreateBrush(); + foreach (var pair in this.lazyParameters) + { + effectBrush.SetSourceParameter(pair.Key, await pair.Value()); + } + + return effectBrush; + } + + /// <summary> + /// Builds the current pipeline and creates a <see cref="SpriteVisual"/> that is applied to the input <see cref="UIElement"/> + /// </summary> + /// <param name="target">The target <see cref="UIElement"/> to apply the brush to</param> + /// <param name="reference">An optional <see cref="UIElement"/> to use to bind the size of the created brush</param> + /// <returns>A <see cref="Task{T}"/> that returns the final <see cref="SpriteVisual"/> instance to use</returns> + public async Task<SpriteVisual> AttachAsync(UIElement target, UIElement? reference = null) + { + SpriteVisual visual = Window.Current.Compositor.CreateSpriteVisual(); + + visual.Brush = await BuildAsync(); + + ElementCompositionPreview.SetElementChildVisual(target, visual); + + if (reference != null) + { + if (reference == target) + { + visual.RelativeSizeAdjustment = Vector2.One; + } + else + { + visual.BindSize(reference); + } + } + + return visual; + } + + /// <summary> + /// Creates a new <see cref="XamlCompositionBrush"/> from the current effects pipeline + /// </summary> + /// <returns>A <see cref="XamlCompositionBrush"/> instance ready to be displayed</returns> + [Pure] + public XamlCompositionBrush AsBrush() + { + return new XamlCompositionBrush(this); + } +} diff --git a/components/Media/src/Visuals/AttachedVisualFactoryBase.cs b/components/Media/src/Visuals/AttachedVisualFactoryBase.cs new file mode 100644 index 00000000..c2d4eaae --- /dev/null +++ b/components/Media/src/Visuals/AttachedVisualFactoryBase.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A type responsible for creating <see cref="Visual"/> instances to attach to target elements. +/// </summary> +public abstract class AttachedVisualFactoryBase : DependencyObject +{ + /// <summary> + /// Creates a <see cref="Visual"/> to attach to the target element. + /// </summary> + /// <param name="element">The target <see cref="UIElement"/> the visual will be attached to.</param> + /// <returns>A <see cref="Visual"/> instance that the caller will attach to the target element.</returns> + public abstract ValueTask<Visual> GetAttachedVisualAsync(UIElement element); +} diff --git a/components/Media/src/Visuals/PipelineVisualFactory.cs b/components/Media/src/Visuals/PipelineVisualFactory.cs new file mode 100644 index 00000000..494fc44b --- /dev/null +++ b/components/Media/src/Visuals/PipelineVisualFactory.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A builder type for <see cref="SpriteVisual"/> instance to apply to UI elements. +/// </summary> +[ContentProperty(Name = nameof(Effects))] +public sealed class PipelineVisualFactory : PipelineVisualFactoryBase +{ + /// <summary> + /// Gets or sets the source for the current pipeline (defaults to a <see cref="BackdropSourceExtension"/> with <see cref="AcrylicBackgroundSource.Backdrop"/> source). + /// </summary> + public PipelineBuilder? Source { get; set; } + + /// <summary> + /// Gets or sets the collection of effects to use in the current pipeline. + /// </summary> + public IList<PipelineEffect> Effects + { + get + { + if (GetValue(EffectsProperty) is not IList<PipelineEffect> effects) + { + effects = new List<PipelineEffect>(); + + SetValue(EffectsProperty, effects); + } + + return effects; + } + set => SetValue(EffectsProperty, value); + } + + /// <summary> + /// Identifies the <seealso cref="Effects"/> dependency property. + /// </summary> + public static readonly DependencyProperty EffectsProperty = DependencyProperty.Register( + nameof(Effects), + typeof(IList<PipelineEffect>), + typeof(PipelineVisualFactory), + new PropertyMetadata(null)); + + /// <inheritdoc/> + public override async ValueTask<Visual> GetAttachedVisualAsync(UIElement element) + { + var visual = (SpriteVisual)await base.GetAttachedVisualAsync(element); + + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(visual.Brush); + } + + return visual; + } + + /// <inheritdoc/> + protected override PipelineBuilder OnPipelineRequested() + { + PipelineBuilder builder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + builder = effect.AppendToBuilder(builder); + } + + return builder; + } +} diff --git a/components/Media/src/Visuals/PipelineVisualFactoryBase.cs b/components/Media/src/Visuals/PipelineVisualFactoryBase.cs new file mode 100644 index 00000000..938546c4 --- /dev/null +++ b/components/Media/src/Visuals/PipelineVisualFactoryBase.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// <summary> +/// A base class that extends <see cref="AttachedVisualFactoryBase"/> by leveraging the <see cref="PipelineBuilder"/> APIs. +/// </summary> +public abstract class PipelineVisualFactoryBase : AttachedVisualFactoryBase +{ + /// <inheritdoc/> + public override async ValueTask<Visual> GetAttachedVisualAsync(UIElement element) + { + var visual = ElementCompositionPreview.GetElementVisual(element).Compositor.CreateSpriteVisual(); + + visual.Brush = await OnPipelineRequested().BuildAsync(); + + return visual; + } + + /// <summary> + /// A method that builds and returns the <see cref="PipelineBuilder"/> pipeline to use in the current instance. + /// </summary> + /// <returns>A <see cref="PipelineBuilder"/> instance to create the <see cref="Visual"/> to display.</returns> + protected abstract PipelineBuilder OnPipelineRequested(); +} diff --git a/components/Media/tests/Media.Tests.projitems b/components/Media/tests/Media.Tests.projitems new file mode 100644 index 00000000..f6bb5968 --- /dev/null +++ b/components/Media/tests/Media.Tests.projitems @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' < '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <HasSharedItems>true</HasSharedItems> + <SharedGUID>29B0EAEF-DDC3-461E-BE63-4052976510E8</SharedGUID> + </PropertyGroup> + <PropertyGroup Label="Configuration"> + <Import_RootNamespace>MediaExperiment.Tests</Import_RootNamespace> + </PropertyGroup> +</Project> \ No newline at end of file diff --git a/components/Media/tests/Media.Tests.shproj b/components/Media/tests/Media.Tests.shproj new file mode 100644 index 00000000..a14f5f00 --- /dev/null +++ b/components/Media/tests/Media.Tests.shproj @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Label="Globals"> + <ProjectGuid>29B0EAEF-DDC3-461E-BE63-4052976510E8</ProjectGuid> + <MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion> + </PropertyGroup> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" /> + <PropertyGroup /> + <Import Project="Media.Tests.projitems" Label="Shared" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" /> +</Project> diff --git a/tooling b/tooling index a852f23d..38728ba6 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit a852f23dabb110b7a51c068662309d00834d90a1 +Subproject commit 38728ba616661853b3bbcec9269ab1e362daa72a