diff --git a/Microsoft.Toolkit/Attributes/DoesNotReturnAttribute.cs b/Microsoft.Toolkit.Diagnostics/Attributes/DoesNotReturnAttribute.cs similarity index 100% rename from Microsoft.Toolkit/Attributes/DoesNotReturnAttribute.cs rename to Microsoft.Toolkit.Diagnostics/Attributes/DoesNotReturnAttribute.cs diff --git a/Microsoft.Toolkit/Attributes/DoesNotReturnIfAttribute.cs b/Microsoft.Toolkit.Diagnostics/Attributes/DoesNotReturnIfAttribute.cs similarity index 100% rename from Microsoft.Toolkit/Attributes/DoesNotReturnIfAttribute.cs rename to Microsoft.Toolkit.Diagnostics/Attributes/DoesNotReturnIfAttribute.cs diff --git a/Microsoft.Toolkit/Attributes/NotNullAttribute.cs b/Microsoft.Toolkit.Diagnostics/Attributes/NotNullAttribute.cs similarity index 100% rename from Microsoft.Toolkit/Attributes/NotNullAttribute.cs rename to Microsoft.Toolkit.Diagnostics/Attributes/NotNullAttribute.cs diff --git a/Microsoft.Toolkit/Attributes/SkipLocalsInitAttribute.cs b/Microsoft.Toolkit.Diagnostics/Attributes/SkipLocalsInitAttribute.cs similarity index 100% rename from Microsoft.Toolkit/Attributes/SkipLocalsInitAttribute.cs rename to Microsoft.Toolkit.Diagnostics/Attributes/SkipLocalsInitAttribute.cs diff --git a/Microsoft.Toolkit/Extensions/TypeExtensions.cs b/Microsoft.Toolkit.Diagnostics/Extensions/TypeExtensions.cs similarity index 100% rename from Microsoft.Toolkit/Extensions/TypeExtensions.cs rename to Microsoft.Toolkit.Diagnostics/Extensions/TypeExtensions.cs diff --git a/Microsoft.Toolkit/Extensions/ValueTypeExtensions.cs b/Microsoft.Toolkit.Diagnostics/Extensions/ValueTypeExtensions.cs similarity index 100% rename from Microsoft.Toolkit/Extensions/ValueTypeExtensions.cs rename to Microsoft.Toolkit.Diagnostics/Extensions/ValueTypeExtensions.cs diff --git a/Microsoft.Toolkit/Diagnostics/Generated/Guard.Collection.g.cs b/Microsoft.Toolkit.Diagnostics/Generated/Guard.Collection.g.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/Guard.Collection.g.cs rename to Microsoft.Toolkit.Diagnostics/Generated/Guard.Collection.g.cs diff --git a/Microsoft.Toolkit/Diagnostics/Generated/Guard.Collection.tt b/Microsoft.Toolkit.Diagnostics/Generated/Guard.Collection.tt similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/Guard.Collection.tt rename to Microsoft.Toolkit.Diagnostics/Generated/Guard.Collection.tt diff --git a/Microsoft.Toolkit/Diagnostics/Generated/Guard.Comparable.Numeric.g.cs b/Microsoft.Toolkit.Diagnostics/Generated/Guard.Comparable.Numeric.g.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/Guard.Comparable.Numeric.g.cs rename to Microsoft.Toolkit.Diagnostics/Generated/Guard.Comparable.Numeric.g.cs diff --git a/Microsoft.Toolkit/Diagnostics/Generated/Guard.Comparable.Numeric.tt b/Microsoft.Toolkit.Diagnostics/Generated/Guard.Comparable.Numeric.tt similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/Guard.Comparable.Numeric.tt rename to Microsoft.Toolkit.Diagnostics/Generated/Guard.Comparable.Numeric.tt diff --git a/Microsoft.Toolkit/Diagnostics/Generated/Guard.md b/Microsoft.Toolkit.Diagnostics/Generated/Guard.md similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/Guard.md rename to Microsoft.Toolkit.Diagnostics/Generated/Guard.md diff --git a/Microsoft.Toolkit/Diagnostics/Generated/ThrowHelper.Collection.g.cs b/Microsoft.Toolkit.Diagnostics/Generated/ThrowHelper.Collection.g.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/ThrowHelper.Collection.g.cs rename to Microsoft.Toolkit.Diagnostics/Generated/ThrowHelper.Collection.g.cs diff --git a/Microsoft.Toolkit/Diagnostics/Generated/ThrowHelper.Collection.tt b/Microsoft.Toolkit.Diagnostics/Generated/ThrowHelper.Collection.tt similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/ThrowHelper.Collection.tt rename to Microsoft.Toolkit.Diagnostics/Generated/ThrowHelper.Collection.tt diff --git a/Microsoft.Toolkit/Diagnostics/Generated/TypeInfo.ttinclude b/Microsoft.Toolkit.Diagnostics/Generated/TypeInfo.ttinclude similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Generated/TypeInfo.ttinclude rename to Microsoft.Toolkit.Diagnostics/Generated/TypeInfo.ttinclude diff --git a/Microsoft.Toolkit/Diagnostics/Guard.Comparable.Generic.cs b/Microsoft.Toolkit.Diagnostics/Guard.Comparable.Generic.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Guard.Comparable.Generic.cs rename to Microsoft.Toolkit.Diagnostics/Guard.Comparable.Generic.cs diff --git a/Microsoft.Toolkit/Diagnostics/Guard.Comparable.Numeric.cs b/Microsoft.Toolkit.Diagnostics/Guard.Comparable.Numeric.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Guard.Comparable.Numeric.cs rename to Microsoft.Toolkit.Diagnostics/Guard.Comparable.Numeric.cs diff --git a/Microsoft.Toolkit/Diagnostics/Guard.IO.cs b/Microsoft.Toolkit.Diagnostics/Guard.IO.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Guard.IO.cs rename to Microsoft.Toolkit.Diagnostics/Guard.IO.cs diff --git a/Microsoft.Toolkit/Diagnostics/Guard.String.cs b/Microsoft.Toolkit.Diagnostics/Guard.String.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Guard.String.cs rename to Microsoft.Toolkit.Diagnostics/Guard.String.cs diff --git a/Microsoft.Toolkit/Diagnostics/Guard.Tasks.cs b/Microsoft.Toolkit.Diagnostics/Guard.Tasks.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Guard.Tasks.cs rename to Microsoft.Toolkit.Diagnostics/Guard.Tasks.cs diff --git a/Microsoft.Toolkit/Diagnostics/Guard.cs b/Microsoft.Toolkit.Diagnostics/Guard.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Guard.cs rename to Microsoft.Toolkit.Diagnostics/Guard.cs diff --git a/Microsoft.Toolkit/Diagnostics/Internals/Guard.Collection.Generic.ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/Internals/Guard.Collection.Generic.ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Internals/Guard.Collection.Generic.ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/Internals/Guard.Collection.Generic.ThrowHelper.cs diff --git a/Microsoft.Toolkit/Diagnostics/Internals/Guard.Comparable.Generic.ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/Internals/Guard.Comparable.Generic.ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Internals/Guard.Comparable.Generic.ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/Internals/Guard.Comparable.Generic.ThrowHelper.cs diff --git a/Microsoft.Toolkit/Diagnostics/Internals/Guard.Comparable.Numeric.ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/Internals/Guard.Comparable.Numeric.ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Internals/Guard.Comparable.Numeric.ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/Internals/Guard.Comparable.Numeric.ThrowHelper.cs diff --git a/Microsoft.Toolkit/Diagnostics/Internals/Guard.IO.ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/Internals/Guard.IO.ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Internals/Guard.IO.ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/Internals/Guard.IO.ThrowHelper.cs diff --git a/Microsoft.Toolkit/Diagnostics/Internals/Guard.String.ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/Internals/Guard.String.ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Internals/Guard.String.ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/Internals/Guard.String.ThrowHelper.cs diff --git a/Microsoft.Toolkit/Diagnostics/Internals/Guard.Tasks.ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/Internals/Guard.Tasks.ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Internals/Guard.Tasks.ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/Internals/Guard.Tasks.ThrowHelper.cs diff --git a/Microsoft.Toolkit/Diagnostics/Internals/Guard.ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/Internals/Guard.ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/Internals/Guard.ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/Internals/Guard.ThrowHelper.cs diff --git a/Microsoft.Toolkit.Diagnostics/Microsoft.Toolkit.Diagnostics.csproj b/Microsoft.Toolkit.Diagnostics/Microsoft.Toolkit.Diagnostics.csproj new file mode 100644 index 00000000000..4d541b559e3 --- /dev/null +++ b/Microsoft.Toolkit.Diagnostics/Microsoft.Toolkit.Diagnostics.csproj @@ -0,0 +1,97 @@ + + + + netstandard1.4;netstandard2.0;netstandard2.1;net5.0 + 9.0 + true + enable + Windows Community Toolkit Diagnostics .NET Standard + + This package includes .NET Standard code only helpers such as: + - Guard: Helper methods to verify conditions when running code. + - ThrowHelper: Helper methods to efficiently throw exceptions. + + UWP Toolkit Windows IncrementalLoadingCollection String Array extensions helpers + + + + + + + + + + + + + + + + + + + + + NETSTANDARD2_1_OR_GREATER + + + + + + + + + + NETSTANDARD2_1_OR_GREATER + + + + + + + TextTemplatingFileGenerator + Guard.Comparable.Numeric.g.cs + + + TextTemplatingFileGenerator + Guard.Collection.g.cs + + + TextTemplatingFileGenerator + ThrowHelper.Collection.g.cs + + + TextTemplatingFileGenerator + TypeInfo.g.cs + + + + + + + + + + + True + True + Guard.Comparable.Numeric.tt + + + True + True + Guard.Collection.tt + + + True + True + ThrowHelper.Collection.tt + + + True + True + TypeInfo.ttinclude + + + + diff --git a/Microsoft.Toolkit/Diagnostics/ThrowHelper.Generic.cs b/Microsoft.Toolkit.Diagnostics/ThrowHelper.Generic.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/ThrowHelper.Generic.cs rename to Microsoft.Toolkit.Diagnostics/ThrowHelper.Generic.cs diff --git a/Microsoft.Toolkit/Diagnostics/ThrowHelper.cs b/Microsoft.Toolkit.Diagnostics/ThrowHelper.cs similarity index 100% rename from Microsoft.Toolkit/Diagnostics/ThrowHelper.cs rename to Microsoft.Toolkit.Diagnostics/ThrowHelper.cs diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs index 5eb18772cb8..89781377df8 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.2D.cs @@ -11,7 +11,6 @@ #endif using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; -using Microsoft.Toolkit.HighPerformance.Memory; using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; namespace Microsoft.Toolkit.HighPerformance.Extensions diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs index 5c683047593..77ad9983a6e 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs @@ -10,7 +10,6 @@ using Microsoft.Toolkit.HighPerformance.Buffers.Internals; #endif using Microsoft.Toolkit.HighPerformance.Helpers.Internals; -using Microsoft.Toolkit.HighPerformance.Memory; using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; namespace Microsoft.Toolkit.HighPerformance.Extensions diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs index 4fd99b2303b..e4fa0a6df73 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/BoolExtensions.cs @@ -23,7 +23,14 @@ public static class BoolExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe byte ToByte(this bool flag) { - return *(byte*)&flag; + // Whenever we need to take the address of an argument, we make a local copy first. + // This will be removed by the JIT anyway, but it can help produce better codegen and + // remove unwanted stack spills if the caller is using constant arguments. This is + // because taking the address of an argument can interfere with some of the flow + // analysis executed by the JIT, which can in some cases block constant propagation. + bool copy = flag; + + return *(byte*)© } /// @@ -58,7 +65,8 @@ public static unsafe int ToInt(this bool flag) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe int ToBitwiseMask32(this bool flag) { - byte rangeFlag = *(byte*)&flag; + bool copy = flag; + byte rangeFlag = *(byte*)© int negativeFlag = rangeFlag - 1, mask = ~negativeFlag; @@ -77,7 +85,8 @@ public static unsafe int ToBitwiseMask32(this bool flag) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe long ToBitwiseMask64(this bool flag) { - byte rangeFlag = *(byte*)&flag; + bool copy = flag; + byte rangeFlag = *(byte*)© long negativeFlag = (long)rangeFlag - 1, mask = ~negativeFlag; diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs index a175a02b577..411fb5f390b 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -7,9 +7,6 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -#if SPAN_RUNTIME_SUPPORT -using Microsoft.Toolkit.HighPerformance.Memory; -#endif using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index d9d996717b0..5e4a07df9ea 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -10,9 +10,6 @@ using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Buffers.Internals; using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces; -#if SPAN_RUNTIME_SUPPORT -using Microsoft.Toolkit.HighPerformance.Memory; -#endif using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream; namespace Microsoft.Toolkit.HighPerformance.Extensions diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index bbfb67c4721..2f81a359ff4 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -8,9 +8,6 @@ using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; -#if SPAN_RUNTIME_SUPPORT -using Microsoft.Toolkit.HighPerformance.Memory; -#endif namespace Microsoft.Toolkit.HighPerformance.Extensions { diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs index d70c743f56b..8ad885e0168 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -8,9 +8,6 @@ using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Enumerables; using Microsoft.Toolkit.HighPerformance.Helpers.Internals; -#if SPAN_RUNTIME_SUPPORT -using Microsoft.Toolkit.HighPerformance.Memory; -#endif namespace Microsoft.Toolkit.HighPerformance.Extensions { diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/BitHelper.cs b/Microsoft.Toolkit.HighPerformance/Helpers/BitHelper.cs index e0411b2d8a9..3e1f5ea876e 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/BitHelper.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/BitHelper.cs @@ -209,8 +209,9 @@ public static unsafe uint SetFlag(uint value, int n, bool flag) // and perform an OR with the resulting value of the previous // operation. This will always guaranteed to work, thanks to the // initial code clearing that bit before setting it again. + bool copy = flag; uint - flag32 = *(byte*)&flag, + flag32 = *(byte*)©, shift = flag32 << n, or = and | shift; @@ -378,8 +379,9 @@ public static unsafe ulong SetFlag(ulong value, int n, bool flag) ulong bit = 1ul << n, not = ~bit, - and = value & not, - flag64 = *(byte*)&flag, + and = value & not; + bool copy = flag; + ulong flag64 = *(byte*)©, shift = flag64 << n, or = and | shift; diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs index 99ff3bbd5a1..2aed6d2e214 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs @@ -5,7 +5,6 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Microsoft.Toolkit.HighPerformance.Memory; namespace Microsoft.Toolkit.HighPerformance.Helpers { diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs index d25d9dfee47..31f3ea45fa0 100644 --- a/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs +++ b/Microsoft.Toolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs @@ -5,7 +5,6 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Microsoft.Toolkit.HighPerformance.Memory; namespace Microsoft.Toolkit.HighPerformance.Helpers { diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Memory2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/Memory2D{T}.cs index 4d1a3c69634..bdd425a99e9 100644 --- a/Microsoft.Toolkit.HighPerformance/Memory/Memory2D{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Memory/Memory2D{T}.cs @@ -19,7 +19,7 @@ #pragma warning disable CA2231 -namespace Microsoft.Toolkit.HighPerformance.Memory +namespace Microsoft.Toolkit.HighPerformance { /// /// represents a 2D region of arbitrary memory. It is to @@ -894,7 +894,7 @@ public override int GetHashCode() /// public override string ToString() { - return $"Microsoft.Toolkit.HighPerformance.Memory.Memory2D<{typeof(T)}>[{this.height}, {this.width}]"; + return $"Microsoft.Toolkit.HighPerformance.Memory2D<{typeof(T)}>[{this.height}, {this.width}]"; } /// diff --git a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs index 4c5fe4dfff4..9f3ea970b86 100644 --- a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs @@ -19,7 +19,7 @@ #pragma warning disable CA2231 -namespace Microsoft.Toolkit.HighPerformance.Memory +namespace Microsoft.Toolkit.HighPerformance { /// /// A readonly version of . @@ -907,7 +907,7 @@ public override int GetHashCode() /// public override string ToString() { - return $"Microsoft.Toolkit.HighPerformance.Memory.ReadOnlyMemory2D<{typeof(T)}>[{this.height}, {this.width}]"; + return $"Microsoft.Toolkit.HighPerformance.ReadOnlyMemory2D<{typeof(T)}>[{this.height}, {this.width}]"; } /// diff --git a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.Enumerator.cs b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.Enumerator.cs index 30dfcbc6406..24f6e504462 100644 --- a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.Enumerator.cs +++ b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.Enumerator.cs @@ -13,7 +13,7 @@ using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; #endif -namespace Microsoft.Toolkit.HighPerformance.Memory +namespace Microsoft.Toolkit.HighPerformance { /// public readonly ref partial struct ReadOnlySpan2D diff --git a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.cs index 11b762ebaa3..46564207512 100644 --- a/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Memory/ReadOnlySpan2D{T}.cs @@ -17,7 +17,7 @@ #pragma warning disable CS0809, CA1065 -namespace Microsoft.Toolkit.HighPerformance.Memory +namespace Microsoft.Toolkit.HighPerformance { /// /// A readonly version of . @@ -974,7 +974,7 @@ public override int GetHashCode() /// public override string ToString() { - return $"Microsoft.Toolkit.HighPerformance.Memory.ReadOnlySpan2D<{typeof(T)}>[{Height}, {this.width}]"; + return $"Microsoft.Toolkit.HighPerformance.ReadOnlySpan2D<{typeof(T)}>[{Height}, {this.width}]"; } /// diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.Enumerator.cs b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.Enumerator.cs index fc5baf9b930..99e796d7a79 100644 --- a/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.Enumerator.cs +++ b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.Enumerator.cs @@ -13,7 +13,7 @@ using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; #endif -namespace Microsoft.Toolkit.HighPerformance.Memory +namespace Microsoft.Toolkit.HighPerformance { /// public readonly ref partial struct Span2D diff --git a/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.cs b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.cs index 9d57eb334e5..7f9009ad947 100644 --- a/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.cs +++ b/Microsoft.Toolkit.HighPerformance/Memory/Span2D{T}.cs @@ -17,7 +17,7 @@ #pragma warning disable CS0809, CA1065 -namespace Microsoft.Toolkit.HighPerformance.Memory +namespace Microsoft.Toolkit.HighPerformance { /// /// represents a 2D region of arbitrary memory. Like the type, @@ -1130,7 +1130,7 @@ public override int GetHashCode() /// public override string ToString() { - return $"Microsoft.Toolkit.HighPerformance.Memory.Span2D<{typeof(T)}>[{Height}, {this.width}]"; + return $"Microsoft.Toolkit.HighPerformance.Span2D<{typeof(T)}>[{Height}, {this.width}]"; } /// diff --git a/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj b/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj index c980f7c4205..2d7fa085a7a 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj +++ b/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj @@ -35,6 +35,11 @@ + + + + + diff --git a/Microsoft.Toolkit.Uwp.Notifications/Properties/Microsoft.Toolkit.Uwp.Notifications.rd.xml b/Microsoft.Toolkit.Uwp.Notifications/Properties/Microsoft.Toolkit.Uwp.Notifications.rd.xml new file mode 100644 index 00000000000..f4e4b4f2b45 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Notifications/Properties/Microsoft.Toolkit.Uwp.Notifications.rd.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs index 32e50870076..8e6c55a86c9 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs @@ -112,7 +112,7 @@ public ToastContentBuilder AddButton(string content, ToastActivationType activat /// The current instance of public ToastContentBuilder AddButton(IToastButton button) { - if (button is ToastButton toastButton && toastButton.Content == null) + if (button is ToastButton toastButton && toastButton.Content == null && toastButton.NeedsContent()) { throw new InvalidOperationException("Content is required on button."); } diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs index a564d274984..feb023faefe 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs @@ -20,6 +20,17 @@ public sealed class ToastButton : private bool _usingCustomArguments; + private bool _usingSnoozeActivation; + private string _snoozeSelectionBoxId; + + private bool _usingDismissActivation; + + internal bool NeedsContent() + { + // Snooze/dismiss buttons don't need content (the system will auto-add the localized strings). + return !_usingDismissActivation && !_usingSnoozeActivation; + } + /// /// Initializes a new instance of the class. /// @@ -213,6 +224,11 @@ private ToastButton AddArgumentHelper(string key, string value) throw new InvalidOperationException("You cannot use the AddArgument methods when using protocol activation."); } + if (_usingDismissActivation || _usingSnoozeActivation) + { + throw new InvalidOperationException("You cannot use the AddArgument methods when using dismiss or snooze activation."); + } + bool alreadyExists = _arguments.ContainsKey(key); _arguments[key] = value; @@ -314,6 +330,48 @@ public ToastButton SetAfterActivationBehavior(ToastAfterActivationBehavior after return this; } + /// + /// Configures the button to use system snooze activation when the button is clicked, using the default system snooze time. + /// + /// The current instance of + public ToastButton SetSnoozeActivation() + { + return SetSnoozeActivation(null); + } + + /// + /// Configures the button to use system snooze activation when the button is clicked, with a snooze time defined by the specified selection box. + /// + /// The ID of an existing which allows the user to pick a custom snooze time. The ID's of the s inside the selection box must represent the snooze interval in minutes. For example, if the user selects an item that has an ID of "120", then the notification will be snoozed for 2 hours. When the user clicks this button, if you specified a SelectionBoxId, the system will parse the ID of the selected item and snooze by that amount of minutes. + /// The current instance of + public ToastButton SetSnoozeActivation(string selectionBoxId) + { + if (_arguments.Count > 0) + { + throw new InvalidOperationException($"{nameof(SetSnoozeActivation)} cannot be used in conjunction with ${nameof(AddArgument)}."); + } + + _usingSnoozeActivation = true; + _snoozeSelectionBoxId = selectionBoxId; + + return this; + } + + /// + /// Configures the button to use system dismiss activation when the button is clicked (the toast will simply dismiss rather than activating). + /// + /// The current instance of + public ToastButton SetDismissActivation() + { + if (_arguments.Count > 0) + { + throw new InvalidOperationException($"{nameof(SetDismissActivation)} cannot be used in conjunction with ${nameof(AddArgument)}."); + } + + _usingDismissActivation = true; + return this; + } + /// /// Sets an identifier used in telemetry to identify your category of action. This should be something like "Delete", "Reply", or "Archive". In the upcoming toast telemetry dashboard in Dev Center, you will be able to view how frequently your actions are being clicked. /// @@ -349,7 +407,7 @@ public ToastButton SetTextBoxId(string textBoxId) internal bool CanAddArguments() { - return ActivationType != ToastActivationType.Protocol && !_usingCustomArguments; + return ActivationType != ToastActivationType.Protocol && !_usingCustomArguments && !_usingDismissActivation && !_usingSnoozeActivation; } internal bool ContainsArgument(string key) @@ -362,13 +420,44 @@ internal Element_ToastAction ConvertToElement() var el = new Element_ToastAction() { Content = Content, - Arguments = Arguments, - ActivationType = Element_Toast.ConvertActivationType(ActivationType), ImageUri = ImageUri, InputId = TextBoxId, HintActionId = HintActionId }; + if (_usingSnoozeActivation) + { + el.ActivationType = Element_ToastActivationType.System; + el.Arguments = "snooze"; + + if (_snoozeSelectionBoxId != null) + { + el.InputId = _snoozeSelectionBoxId; + } + + // Content needs to be specified as empty for auto-generated Snooze content + if (el.Content == null) + { + el.Content = string.Empty; + } + } + else if (_usingDismissActivation) + { + el.ActivationType = Element_ToastActivationType.System; + el.Arguments = "dismiss"; + + // Content needs to be specified as empty for auto-generated Dismiss content + if (el.Content == null) + { + el.Content = string.Empty; + } + } + else + { + el.ActivationType = Element_Toast.ConvertActivationType(ActivationType); + el.Arguments = Arguments; + } + ActivationOptions?.PopulateElement(el); return el; diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj index 23cb14bd7ac..d55194bd8b5 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj +++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj @@ -272,12 +272,14 @@ + + @@ -496,6 +498,7 @@ AutoFocusBehaviorPage.xaml + ColorPickerButtonPage.xaml @@ -505,6 +508,9 @@ EnumValuesExtensionPage.xaml + + CanvasPathGeometryPage.xaml + FocusBehaviorPage.xaml @@ -990,6 +996,13 @@ MSBuild:Compile Designer + + Designer + + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometry.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometry.png new file mode 100644 index 00000000000..5c909a581e1 Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometry.png differ diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml new file mode 100644 index 00000000000..44a17d85327 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItem.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItem.cs new file mode 100644 index 00000000000..8cf1bb24ba2 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItem.cs @@ -0,0 +1,126 @@ +// 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.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// A to be displayed in a + /// + [TemplatePart(Name = "PrimaryItemsControl", Type = typeof(ItemsControl))] + [TemplatePart(Name = "MoreButton", Type = typeof(Button))] + public class TabbedCommandBarItem : CommandBar + { + private ItemsControl _primaryItemsControl; + private Button _moreButton; + + /// + /// Initializes a new instance of the class. + /// + public TabbedCommandBarItem() + { + DefaultStyleKey = typeof(TabbedCommandBarItem); + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register( + nameof(Header), + typeof(object), + typeof(TabbedCommandBarItem), + new PropertyMetadata(string.Empty)); + + /// + /// Gets or sets the text or to display in the header of this ribbon tab. + /// + public object Header + { + get => (object)GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty IsContextualProperty = DependencyProperty.Register( + nameof(IsContextual), + typeof(bool), + typeof(TabbedCommandBarItem), + new PropertyMetadata(false)); + + /// + /// Gets or sets a value indicating whether this tab is contextual. + /// + public bool IsContextual + { + get => (bool)GetValue(IsContextualProperty); + set => SetValue(IsContextualProperty, value); + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty OverflowButtonAlignmentProperty = DependencyProperty.Register( + nameof(OverflowButtonAlignment), + typeof(HorizontalAlignment), + typeof(TabbedCommandBarItem), + new PropertyMetadata(HorizontalAlignment.Left)); + + /// + /// Gets or sets a value indicating the alignment of the command overflow button. + /// + public HorizontalAlignment OverflowButtonAlignment + { + get => (HorizontalAlignment)GetValue(OverflowButtonAlignmentProperty); + set => SetValue(OverflowButtonAlignmentProperty, value); + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty CommandAlignmentProperty = DependencyProperty.Register( + nameof(CommandAlignment), + typeof(HorizontalAlignment), + typeof(TabbedCommandBarItem), + new PropertyMetadata(HorizontalAlignment.Stretch)); + + /// + /// Gets or sets a value indicating the alignment of the commands in the . + /// + public HorizontalAlignment CommandAlignment + { + get => (HorizontalAlignment)GetValue(CommandAlignmentProperty); + set => SetValue(CommandAlignmentProperty, value); + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _primaryItemsControl = GetTemplateChild("PrimaryItemsControl") as ItemsControl; + if (_primaryItemsControl != null) + { + _primaryItemsControl.HorizontalAlignment = CommandAlignment; + RegisterPropertyChangedCallback(CommandAlignmentProperty, (sender, dp) => + { + _primaryItemsControl.HorizontalAlignment = (HorizontalAlignment)sender.GetValue(dp); + }); + } + + _moreButton = GetTemplateChild("MoreButton") as Button; + if (_moreButton != null) + { + _moreButton.HorizontalAlignment = OverflowButtonAlignment; + RegisterPropertyChangedCallback(OverflowButtonAlignmentProperty, (sender, dp) => + { + _moreButton.HorizontalAlignment = (HorizontalAlignment)sender.GetValue(dp); + }); + } + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItem.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItem.xaml new file mode 100644 index 00000000000..d05424ee99e --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItem.xaml @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItemTemplateSelector.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItemTemplateSelector.cs new file mode 100644 index 00000000000..0c68a9b35f5 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/TabbedCommandBar/TabbedCommandBarItemTemplateSelector.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 Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// used by for determining the style of normal vs. contextual s. + /// + public class TabbedCommandBarItemTemplateSelector : DataTemplateSelector + { + /// + /// Gets or sets the of a normal . + /// + public DataTemplate Normal { get; set; } + + /// + /// Gets or sets the of a contextual . + /// + public DataTemplate Contextual { get; set; } + + /// + protected override DataTemplate SelectTemplateCore(object item) + { + return item is TabbedCommandBarItem t && t.IsContextual ? Contextual : Normal; + } + + /// + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + return SelectTemplateCore(item); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml index 948dca8dc42..86346a7d1f4 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml @@ -9,6 +9,7 @@ + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/VisualStudioToolsManifest.xml b/Microsoft.Toolkit.Uwp.UI.Controls.Core/VisualStudioToolsManifest.xml index 34f1bc813a9..161131ea6dd 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/VisualStudioToolsManifest.xml +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/VisualStudioToolsManifest.xml @@ -12,6 +12,7 @@ + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/ColorPicker/ColorPicker.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/ColorPicker/ColorPicker.cs index cb7398d9791..8dbe534bf94 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/ColorPicker/ColorPicker.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/ColorPicker/ColorPicker.cs @@ -3,14 +3,11 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Globalization; -using Microsoft.Toolkit.Diagnostics; using Microsoft.Toolkit.Uwp.Helpers; using Microsoft.Toolkit.Uwp.UI.Controls.ColorPickerConverters; -using Windows.Foundation; using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -233,10 +230,12 @@ private T GetTemplateChild(string childName, bool isRequired = false) T child = this.GetTemplateChild(childName) as T; if ((child == null) && isRequired) { - ThrowHelper.ThrowArgumentNullException(childName); + ThrowArgumentNullException(); } return child; + + static void ThrowArgumentNullException() => throw new ArgumentNullException(nameof(childName)); } /// diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/RangeSelector/RangeSelector.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/RangeSelector/RangeSelector.cs index 373db695187..518f0de6790 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/RangeSelector/RangeSelector.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/RangeSelector/RangeSelector.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using Microsoft.Toolkit.Uwp.UI.Extensions; +using Microsoft.Toolkit.Uwp.Extensions; using Windows.Foundation; using Windows.System; using Windows.UI.Xaml; diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/InterspersedObservableCollection.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/InterspersedObservableCollection.cs index 74fc28fd5d1..c0c84fef849 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/InterspersedObservableCollection.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/InterspersedObservableCollection.cs @@ -5,11 +5,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Immutable; using System.Collections.Specialized; -using System.ComponentModel; using System.Linq; -using Microsoft.Toolkit.Diagnostics; using Microsoft.Toolkit.Uwp.Helpers; namespace Microsoft.Toolkit.Uwp.UI.Controls @@ -54,9 +51,12 @@ public object this[int index] public InterspersedObservableCollection(object itemsSource) { - Guard.IsAssignableToType(itemsSource, nameof(itemsSource)); + if (!(itemsSource is IList list)) + { + ThrowArgumentException(); + } - ItemsSource = itemsSource as IList; + ItemsSource = list; if (ItemsSource is INotifyCollectionChanged notifier) { @@ -67,6 +67,8 @@ public InterspersedObservableCollection(object itemsSource) }; notifier.CollectionChanged += weakPropertyChangedListener.OnEvent; } + + static void ThrowArgumentException() => throw new ArgumentNullException("The input items source must be assignable to the System.Collections.IList type."); } private void ItemsSource_CollectionChanged(object source, NotifyCollectionChangedEventArgs eventArgs) @@ -192,12 +194,20 @@ private void ReadjustKeys() /// Inner ItemsSource Index. private int ToInnerIndex(int outerIndex) { -#if DEBUG - Guard.IsInRange(outerIndex, 0, Count, nameof(outerIndex)); - Guard.IsFalse(_interspersedObjects.ContainsKey(outerIndex), nameof(outerIndex)); // We can't map a inserted key to the original collection! -#endif + if ((uint)outerIndex >= Count) + { + ThrowArgumentOutOfRangeException(); + } + + if (_interspersedObjects.ContainsKey(outerIndex)) + { + ThrowArgumentException(); + } return outerIndex - _interspersedObjects.Keys.Count(key => key.Value <= outerIndex); + + static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException(nameof(outerIndex)); + static void ThrowArgumentException() => throw new ArgumentException("The outer index can't be inserted as a key to the original collection."); } /// @@ -207,9 +217,10 @@ private int ToInnerIndex(int outerIndex) /// Index into the entire collection. private int ToOuterIndex(int innerIndex) { -#if DEBUG - Guard.IsInRange(innerIndex, 0, ItemsSource.Count, nameof(innerIndex)); -#endif + if ((uint)innerIndex >= ItemsSource.Count) + { + ThrowArgumentOutOfRangeException(); + } var keys = _interspersedObjects.OrderBy(v => v.Key); @@ -226,6 +237,8 @@ private int ToOuterIndex(int innerIndex) } return innerIndex; + + static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException(nameof(innerIndex)); } /// @@ -235,9 +248,10 @@ private int ToOuterIndex(int innerIndex) /// Projected index in the entire collection. private int ToOuterIndexAfterRemoval(int innerIndexToProject) { -#if DEBUG - Guard.IsInRange(innerIndexToProject, 0, ItemsSource.Count + 1, nameof(innerIndexToProject)); -#endif + if ((uint)innerIndexToProject >= ItemsSource.Count + 1) + { + ThrowArgumentOutOfRangeException(); + } //// TODO: Deal with bounds (0 / Count)? Or is it the same? @@ -256,6 +270,8 @@ private int ToOuterIndexAfterRemoval(int innerIndexToProject) } return innerIndexToProject; + + static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException(nameof(innerIndexToProject)); } /// diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/GridSplitter/GridSplitter.Events.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/GridSplitter/GridSplitter.Events.cs index d64d1123afd..131baccd3ad 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/GridSplitter/GridSplitter.Events.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/GridSplitter/GridSplitter.Events.cs @@ -148,6 +148,11 @@ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) var horizontalChange = e.Delta.Translation.X; var verticalChange = e.Delta.Translation.Y; + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } + if (_resizeDirection == GridResizeDirection.Columns) { if (HorizontalMove(horizontalChange)) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/UniformGrid/TakenSpotsReferenceHolder.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/UniformGrid/TakenSpotsReferenceHolder.cs index 9474082e25e..0092a1275cb 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/UniformGrid/TakenSpotsReferenceHolder.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/UniformGrid/TakenSpotsReferenceHolder.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Drawing; -using Microsoft.Toolkit.Diagnostics; namespace Microsoft.Toolkit.Uwp.UI.Controls { @@ -28,9 +27,6 @@ internal sealed class TakenSpotsReferenceHolder /// The number of columns to track. public TakenSpotsReferenceHolder(int rows, int columns) { - Guard.IsGreaterThanOrEqualTo(rows, 0, nameof(rows)); - Guard.IsGreaterThanOrEqualTo(columns, 0, nameof(columns)); - Height = rows; Width = columns; diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/WrapPanel/WrapPanel.Data.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/WrapPanel/WrapPanel.Data.cs index c3112d9bd96..cf8ac47ed52 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/WrapPanel/WrapPanel.Data.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Primitives/WrapPanel/WrapPanel.Data.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using Microsoft.Toolkit.Diagnostics; using Windows.Foundation; using Windows.UI.Xaml.Controls; @@ -63,8 +62,10 @@ private struct UvRect { Orientation.Vertical => new Rect(Position.V, Position.U, Size.V, Size.U), Orientation.Horizontal => new Rect(Position.U, Position.V, Size.U, Size.V), - _ => ThrowHelper.ThrowNotSupportedException("unsupported orientation"), + _ => ThrowArgumentException() }; + + private static Rect ThrowArgumentException() => throw new ArgumentException("The input orientation is not valid."); } private struct Row diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs b/Microsoft.Toolkit.Uwp.UI.Media/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs index b2f2f697824..faaa78dd171 100644 --- a/Microsoft.Toolkit.Uwp.UI.Media/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs +++ b/Microsoft.Toolkit.Uwp.UI.Media/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using Microsoft.Toolkit.Diagnostics; using Microsoft.Toolkit.Uwp.UI.Media; using Windows.UI.Composition; using Windows.UI.Xaml; @@ -50,14 +49,21 @@ public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeS { if (Target is not TEffect target) { - return ThrowHelper.ThrowArgumentNullException("The target effect is null, make sure to set the Target property"); + 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) { - return ThrowHelper.ThrowArgumentNullException( - "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."); + 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.Composition keyFrameBuilder = new( @@ -70,7 +76,7 @@ public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeS CompositionAnimation animation = keyFrameBuilder.GetAnimation(target.Brush!, out _); - return builder.ExternalAnimation(target.Brush, animation); + return builder.ExternalAnimation(target.Brush!, animation); } } } diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasDrawingSessionExtensions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasDrawingSessionExtensions.cs new file mode 100644 index 00000000000..c1d870f08f9 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasDrawingSessionExtensions.cs @@ -0,0 +1,307 @@ +// 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.Brushes; +using Microsoft.Graphics.Canvas.Geometry; +using Windows.Foundation; +using Windows.UI; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Extension methods for CanvasDrawingSession. + /// + public static class CanvasDrawingSessionExtensions + { + /// + /// Draws a circle of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Center of the Circle + /// Radius of the Circle + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawCircle(this CanvasDrawingSession session, Vector2 centerPoint, float radius, ICanvasStroke stroke) + { + session.DrawCircle(centerPoint, radius, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a circle of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Offset of the Center in x axis + /// Ordinate of the Center in the y axis + /// Radius of the Circle + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawCircle(this CanvasDrawingSession session, float x, float y, float radius, ICanvasStroke stroke) + { + session.DrawCircle(x, y, radius, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws an Ellipse of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Center of the Circle + /// Radius in the X axis + /// Radius in the Y axis + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawEllipse(this CanvasDrawingSession session, Vector2 centerPoint, float radiusX, float radiusY, ICanvasStroke stroke) + { + session.DrawEllipse(centerPoint, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws an Ellipse of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Offset of the Center on the x axis + /// Offset of the Center on the y axis + /// Radius in the X axis + /// Radius in the Y axis + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawEllipse(this CanvasDrawingSession session, float x, float y, float radiusX, float radiusY, ICanvasStroke stroke) + { + session.DrawEllipse(x, y, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a geometry relative to the origin, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// CanvasGeometry to render + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawGeometry(this CanvasDrawingSession session, CanvasGeometry geometry, ICanvasStroke stroke) + { + session.DrawGeometry(geometry, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a geometry relative to the specified position, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// CanvasGeometry to render + /// Offset + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawGeometry(this CanvasDrawingSession session, CanvasGeometry geometry, Vector2 offset, ICanvasStroke stroke) + { + session.DrawGeometry(geometry, offset, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a geometry relative to the specified position, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// CanvasGeometry to render + /// Offset on the x axis + /// Offset on the y axis + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawGeometry(this CanvasDrawingSession session, CanvasGeometry geometry, float x, float y, ICanvasStroke stroke) + { + session.DrawGeometry(geometry, x, y, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a line between the specified positions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Starting position of the line + /// Ending position of the line + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawLine(this CanvasDrawingSession session, Vector2 point0, Vector2 point1, ICanvasStroke stroke) + { + session.DrawLine(point0, point1, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a line between the specified positions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Offset of Starting position of the line on x-axis + /// Offset of Starting position of the line on y-axis + /// Offset of Ending position of the line on x-axis + /// Offset of Ending position of the line on y-axis + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawLine(this CanvasDrawingSession session, float x0, float y0, float x1, float y1, ICanvasStroke stroke) + { + session.DrawLine(x0, y0, x1, y1, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Rectangle dimensions + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawRectangle(this CanvasDrawingSession session, Rect rect, ICanvasStroke stroke) + { + session.DrawRectangle(rect, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Rectangle on the x-axis + /// Offset of the top left corner of the Rectangle on the y-axis + /// Width of the Rectangle + /// Height of the Rectangle + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawRectangle(this CanvasDrawingSession session, float x, float y, float w, float h, ICanvasStroke stroke) + { + session.DrawRectangle(x, y, w, h, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a Rounded Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Rectangle dimensions + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawRoundedRectangle(this CanvasDrawingSession session, Rect rect, float radiusX, float radiusY, ICanvasStroke stroke) + { + session.DrawRoundedRectangle(rect, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a Rounded Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Rounded Rectangle on the x-axis + /// Offset of the top left corner of the Rounded Rectangle on the y-axis + /// Width of the Rounded Rectangle + /// Height of the Rounded Rectangle + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawRoundedRectangle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, ICanvasStroke stroke) + { + session.DrawRoundedRectangle(x, y, w, h, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style); + } + + /// + /// Draws a Squircle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Squircle on the x-axis + /// Offset of the top left corner of the Squircle on the y-axis + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, ICanvasStroke stroke) + { + using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY); + session.DrawGeometry(geometry, stroke); + } + + /// + /// Draws a Squircle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Squircle on the x-axis + /// Offset of the top left corner of the Squircle on the y-axis + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// Offset of the Squircle from the origin. + /// CanvasStroke defining the stroke width, the stroke + /// color and stroke style. + public static void DrawSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Vector2 offset, ICanvasStroke stroke) + { + using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY); + session.DrawGeometry(geometry, offset, stroke); + } + + /// + /// Fills a Squircle of the specified dimensions, using the given color. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Squircle on the x-axis + /// Offset of the top left corner of the Squircle on the y-axis + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// Color to fill the Squircle. + public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Color color) + { + using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY); + session.FillGeometry(geometry, color); + } + + /// + /// Fills a Squircle of the specified dimensions, using the given brush. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Squircle on the x-axis + /// Offset of the top left corner of the Squircle on the y-axis + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// Brush to fill the Squircle. + public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, ICanvasBrush brush) + { + using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY); + session.FillGeometry(geometry, brush); + } + + /// + /// Fills a Squircle of the specified dimensions, using the given color at specified offset. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Squircle on the x-axis + /// Offset of the top left corner of the Squircle on the y-axis + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// Offset of the Squircle from the origin. + /// Color to fill the Squircle. + public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Vector2 offset, Color color) + { + using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY); + session.FillGeometry(geometry, offset, color); + } + + /// + /// Fills a Squircle of the specified dimensions, using the given brush at specified offset. + /// + /// CanvasDrawingSession + /// Offset of the top left corner of the Squircle on the x-axis + /// Offset of the top left corner of the Squircle on the y-axis + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x axis + /// Corner Radius on the y axis + /// Offset of the Squircle from the origin. + /// Brush to fill the Squircle. + public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Vector2 offset, ICanvasBrush brush) + { + using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY); + session.FillGeometry(geometry, offset, brush); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathBuilderExtensions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathBuilderExtensions.cs new file mode 100644 index 00000000000..b704ce13f18 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathBuilderExtensions.cs @@ -0,0 +1,462 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Defines extension methods for CanvasPathBuilder. + /// + public static class CanvasPathBuilderExtensions + { + private const float SquircleFactor = 1.125f; + private const float ControlPointFactor = 46f / 64f; + + /// + /// Adds a line in the form of a cubic bezier. The control point of the quadratic bezier will be the endpoint of the line itself. + /// + /// + /// Ending location of the line segment. + public static void AddLineAsQuadraticBezier(this CanvasPathBuilder pathBuilder, Vector2 end) + { + pathBuilder.AddQuadraticBezier(end, end); + } + + /// + /// Adds a line in the form of a cubic bezier. The two control points of the cubic bezier will be the endpoints of the line itself. + /// + /// + /// Starting location of the line segment. + /// Ending location of the line segment. + public static void AddLineAsCubicBezier(this CanvasPathBuilder pathBuilder, Vector2 start, Vector2 end) + { + pathBuilder.AddCubicBezier(start, end, end); + } + + /// + /// Adds a circle figure to the path. + /// + /// + /// Center location of the circle. + /// Radius of the circle. + public static void AddCircleFigure(this CanvasPathBuilder pathBuilder, Vector2 center, float radius) + { + pathBuilder.AddEllipseFigure(center.X, center.Y, radius, radius); + } + + /// + /// Adds a circle figure to the path. + /// + /// + /// X coordinate of the center location of the circle. + /// Y coordinate of the center location of the circle. + /// Radius of the circle. + public static void AddCircleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float radius) + { + pathBuilder.AddEllipseFigure(x, y, radius, radius); + } + + /// + /// Adds an ellipse figure to the path. + /// + /// + /// Center location of the ellipse. + /// Radius of the ellipse on the X-axis. + /// Radius of the ellipse on the Y-axis. + public static void AddEllipseFigure(this CanvasPathBuilder pathBuilder, Vector2 center, float radiusX, float radiusY) + { + pathBuilder.AddEllipseFigure(center.X, center.Y, radiusX, radiusY); + } + + /// + /// Adds an ellipse figure to the path. + /// + /// + /// X coordinate of the center location of the ellipse. + /// Y coordinate of the center location of the ellipse. + /// Radius of the ellipse on the X-axis. + /// Radius of the ellipse on the Y-axis. + public static void AddEllipseFigure(this CanvasPathBuilder pathBuilder, float x, float y, float radiusX, float radiusY) + { + // Sanitize the radiusX by taking the absolute value + radiusX = Math.Abs(radiusX); + + // Sanitize the radiusY by taking the absolute value + radiusY = Math.Abs(radiusY); + + try + { + pathBuilder.BeginFigure(x + radiusX, y); + } + catch (ArgumentException) + { + // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddEllipseFigure() method. + static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddEllipseFigure occurred, " + + "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " + + "before calling CanvasPathBuilder.AddEllipseFigure, to end the previous figure."); + + Throw(); + } + + // First Semi-Ellipse + pathBuilder.AddArc(new Vector2(x - radiusX, y), radiusX, radiusY, Scalar.Pi, CanvasSweepDirection.Clockwise, CanvasArcSize.Large); + + // Second Semi-Ellipse + pathBuilder.AddArc(new Vector2(x + radiusX, y), radiusX, radiusY, Scalar.Pi, CanvasSweepDirection.Clockwise, CanvasArcSize.Large); + + // End Figure + pathBuilder.EndFigure(CanvasFigureLoop.Closed); + } + + /// + /// Adds a n-sided polygon figure to the path. + /// + /// + /// Number of sides of the polygon. + /// Center location of the polygon. + /// Radius of the circle circumscribing the polygon i.e. the distance + /// of each of the vertices of the polygon from the center. + public static void AddPolygonFigure(this CanvasPathBuilder pathBuilder, int numSides, Vector2 center, float radius) + { + pathBuilder.AddPolygonFigure(numSides, center.X, center.Y, radius); + } + + /// + /// Adds a n-sided polygon figure to the path. + /// + /// + /// Number of sides of the polygon. + /// X coordinate of the center location of the polygon. + /// Y coordinate of the center location of the polygon. + /// Radius of the circle circumscribing the polygon i.e. the distance + /// of each of the vertices of the polygon from the center. + public static void AddPolygonFigure(this CanvasPathBuilder pathBuilder, int numSides, float x, float y, float radius) + { + // Sanitize the radius by taking the absolute value + radius = Math.Abs(radius); + + // A polygon should have at least 3 sides + if (numSides <= 2) + { + ThrowArgumentOutOfRangeException(); + } + + // Calculate the first vertex location based on the number of sides + var angle = Scalar.TwoPi / numSides; + var startAngle = numSides % 2 == 1 ? Scalar.PiByTwo : Scalar.PiByTwo - (angle / 2f); + + var startX = x + (float)(radius * Math.Cos(startAngle)); + var startY = y - (float)(radius * Math.Sin(startAngle)); + + try + { + pathBuilder.BeginFigure(startX, startY); + } + catch (ArgumentException) + { + // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method. + static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddPolygonFigure occurred, " + + "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " + + "before calling CanvasPathBuilder.AddPolygonFigure, to end the previous figure."); + + Throw(); + } + + // Add lines to the remaining vertices + for (var i = 1; i < numSides; i++) + { + var posX = x + (float)(radius * Math.Cos(startAngle + (i * angle))); + var posY = y - (float)(radius * Math.Sin(startAngle + (i * angle))); + pathBuilder.AddLine(posX, posY); + } + + // Add a line to the first vertex so that the lines join properly + pathBuilder.AddLine(startX, startY); + + // End the Figure + pathBuilder.EndFigure(CanvasFigureLoop.Closed); + + static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException($"Parameter {nameof(numSides)} must be greater than 2."); + } + + /// + /// Adds a Rectangle to the Path. + /// + /// + /// X offset of the TopLeft corner of the Rectangle + /// Y offset of the TopLeft corner of the Rectangle + /// Width of the Rectangle + /// Height of the Rectangle + public static void AddRectangleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float width, float height) + { + // Sanitize the width by taking the absolute value + width = Math.Abs(width); + + // Sanitize the height by taking the absolute value + height = Math.Abs(height); + + try + { + pathBuilder.BeginFigure(x, y); + } + catch (ArgumentException) + { + // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method. + static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddRectangleFigure occurred, " + + "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " + + "before calling CanvasPathBuilder.AddRectangleFigure, to end the previous figure."); + + Throw(); + } + + // Top Side + pathBuilder.AddLine(x + width, y); + + // Right Side + pathBuilder.AddLine(x + width, y + height); + + // Bottom Side + pathBuilder.AddLine(x, y + height); + + // Left Side + pathBuilder.AddLine(x, y); + + // End the Figure + pathBuilder.EndFigure(CanvasFigureLoop.Closed); + } + + /// + /// Adds a RoundedRectangle to the Path. + /// + /// + /// X offset of the TopLeft corner of the RoundedRectangle + /// Y offset of the TopLeft corner of the RoundedRectangle + /// Width of the RoundedRectangle + /// Height of the RoundedRectangle + /// Corner Radius on the x-axis + /// Corner Radius on the y-axis + public static void AddRoundedRectangleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float width, float height, float radiusX, float radiusY) + { + // Sanitize the width by taking the absolute value + width = Math.Abs(width); + + // Sanitize the height by taking the absolute value + height = Math.Abs(height); + + var rect = new CanvasRoundRect(x, y, width, height, radiusX, radiusY); + pathBuilder.AddRoundedRectangleFigure(ref rect, true); + } + + /// + /// Adds a RoundedRectangle to the Path. (To be used internally) + /// + /// + /// CanvasRoundRect + /// Flag to indicate whether exception should be raised + internal static void AddRoundedRectangleFigure(this CanvasPathBuilder pathBuilder, ref CanvasRoundRect rect, bool raiseException = false) + { + try + { + // Begin path + pathBuilder.BeginFigure(new Vector2(rect.LeftTopX, rect.LeftTopY)); + } + catch (ArgumentException) + { + if (!raiseException) + { + return; + } + + // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method. + static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddRoundedRectangleFigure occurred, " + + "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " + + "before calling CanvasPathBuilder.AddRoundedRectangleFigure, to end the previous figure."); + + Throw(); + } + + // Top line + pathBuilder.AddLine(new Vector2(rect.RightTopX, rect.RightTopY)); + + // Upper-right corner + var radiusX = rect.TopRightX - rect.RightTopX; + var radiusY = rect.TopRightY - rect.RightTopY; + var center = new Vector2(rect.RightTopX, rect.TopRightY); + pathBuilder.AddArc(center, radiusX, radiusY, 3f * Scalar.PiByTwo, Scalar.PiByTwo); + + // Right line + pathBuilder.AddLine(new Vector2(rect.BottomRightX, rect.BottomRightY)); + + // Lower-right corner + radiusX = rect.BottomRightX - rect.RightBottomX; + radiusY = rect.RightBottomY - rect.BottomRightY; + center = new Vector2(rect.RightBottomX, rect.BottomRightY); + pathBuilder.AddArc(center, radiusX, radiusY, 0f, Scalar.PiByTwo); + + // Bottom line + pathBuilder.AddLine(new Vector2(rect.LeftBottomX, rect.LeftBottomY)); + + // Lower-left corner + radiusX = rect.LeftBottomX - rect.BottomLeftX; + radiusY = rect.LeftBottomY - rect.BottomLeftY; + center = new Vector2(rect.LeftBottomX, rect.BottomLeftY); + pathBuilder.AddArc(center, radiusX, radiusY, Scalar.PiByTwo, Scalar.PiByTwo); + + // Left line + pathBuilder.AddLine(new Vector2(rect.TopLeftX, rect.TopLeftY)); + + // Upper-left corner + radiusX = rect.LeftTopX - rect.TopLeftX; + radiusY = rect.TopLeftY - rect.LeftTopY; + center = new Vector2(rect.LeftTopX, rect.TopLeftY); + pathBuilder.AddArc(center, radiusX, radiusY, 2f * Scalar.PiByTwo, Scalar.PiByTwo); + + // End path + pathBuilder.EndFigure(CanvasFigureLoop.Closed); + } + + /// + /// Adds a Squircle to the Path. + /// + /// + /// X offset of the TopLeft corner of the Squircle + /// Y offset of the TopLeft corner of the Squircle + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x-axis + /// Corner Radius on the y-axis + public static void AddSquircleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float width, float height, float radiusX, float radiusY) + { + // Sanitize the width by taking the absolute value + width = Math.Abs(width); + + // Sanitize the height by taking the absolute value + height = Math.Abs(height); + + var rect = new CanvasRoundRect(x, y, width, height, radiusX * SquircleFactor, radiusY * SquircleFactor); + pathBuilder.AddSquircleFigure(ref rect, true); + } + + /// + /// Adds a Squircle to the Path. (To be used internally) + /// + /// + /// CanvasRoundRect + /// Flag to indicate whether exception should be raised + internal static void AddSquircleFigure(this CanvasPathBuilder pathBuilder, ref CanvasRoundRect rect, bool raiseException = false) + { + try + { + // Begin path + pathBuilder.BeginFigure(new Vector2(rect.LeftTopX, rect.LeftTopY)); + } + catch (ArgumentException) + { + if (!raiseException) + { + return; + } + + // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method. + static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddSquircleFigure occurred, " + + "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " + + "before calling CanvasPathBuilder.AddSquircleFigure, to end the previous figure."); + + Throw(); + } + + // Top line + pathBuilder.AddLine(new Vector2(rect.RightTopX, rect.RightTopY)); + + // Upper-right corner + var rightTopControlPoint = new Vector2(rect.RightTopX + ((rect.TopRightX - rect.RightTopX) * ControlPointFactor), rect.RightTopY); + var topRightControlPoint = new Vector2(rect.TopRightX, rect.TopRightY - ((rect.TopRightY - rect.RightTopY) * ControlPointFactor)); + + // Top Right Curve + pathBuilder.AddCubicBezier(rightTopControlPoint, topRightControlPoint, new Vector2(rect.TopRightX, rect.TopRightY)); + + // Right line + pathBuilder.AddLine(new Vector2(rect.BottomRightX, rect.BottomRightY)); + + // Lower-right corner + var bottomRightControlPoint = new Vector2(rect.BottomRightX, rect.BottomRightY + ((rect.RightBottomY - rect.BottomRightY) * ControlPointFactor)); + var rightBottomControlPoint = new Vector2(rect.RightBottomX + ((rect.BottomRightX - rect.RightBottomX) * ControlPointFactor), rect.RightBottomY); + + // Bottom Right Curve + pathBuilder.AddCubicBezier(bottomRightControlPoint, rightBottomControlPoint, new Vector2(rect.RightBottomX, rect.RightBottomY)); + + // Bottom line + pathBuilder.AddLine(new Vector2(rect.LeftBottomX, rect.LeftBottomY)); + + // Lower-left corner + var leftBottomControlPoint = new Vector2(rect.LeftBottomX - ((rect.LeftBottomX - rect.BottomLeftX) * ControlPointFactor), rect.LeftBottomY); + var bottomLeftControlPoint = new Vector2(rect.BottomLeftX, rect.BottomLeftY + ((rect.LeftBottomY - rect.BottomLeftY) * ControlPointFactor)); + + // Bottom Left Curve + pathBuilder.AddCubicBezier(leftBottomControlPoint, bottomLeftControlPoint, new Vector2(rect.BottomLeftX, rect.BottomLeftY)); + + // Left line + pathBuilder.AddLine(new Vector2(rect.TopLeftX, rect.TopLeftY)); + + // Upper-left corner + var topLeftControlPoint = new Vector2(rect.TopLeftX, rect.TopLeftY - ((rect.TopLeftY - rect.LeftTopY) * ControlPointFactor)); + var leftTopControlPoint = new Vector2(rect.LeftTopX - ((rect.LeftTopX - rect.TopLeftX) * ControlPointFactor), rect.LeftTopY); + + // Top Left Curve + pathBuilder.AddCubicBezier(topLeftControlPoint, leftTopControlPoint, new Vector2(rect.LeftTopX, rect.LeftTopY)); + + // End path + pathBuilder.EndFigure(CanvasFigureLoop.Closed); + } + + /// + /// Builds a path with the given collection of points. + /// + /// + /// Specifies whether the figure is open or closed. + /// This affects the appearance of fills and strokes, as well as geometry operations. + /// Collection of Vector2 points on the path. + /// object + public static CanvasPathBuilder BuildPathWithLines(this CanvasPathBuilder builder, CanvasFigureLoop canvasFigureLoop, IEnumerable points) + { + var first = true; + + foreach (var point in points) + { + if (first) + { + builder.BeginFigure(point); + first = false; + } + else + { + builder.AddLine(point); + } + } + + builder.EndFigure(canvasFigureLoop); + return builder; + } + + /// + /// Builds a path with the given collection of points in the (x, y) pattern. + /// + /// + /// Specifies whether the figure is open or closed. + /// This affects the appearance of fills and strokes, as well as geometry operations. + /// Collection of points in the (x, y) pattern on the path. + /// object + public static CanvasPathBuilder BuildPathWithLines(this CanvasPathBuilder builder, CanvasFigureLoop canvasFigureLoop, IEnumerable<(float x, float y)> nodes) + { + var vectors = nodes.Select(n => new Vector2(n.x, n.y)); + return BuildPathWithLines(builder, canvasFigureLoop, vectors); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathGeometry.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathGeometry.cs new file mode 100644 index 00000000000..5a6bad1c378 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathGeometry.cs @@ -0,0 +1,140 @@ +// 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 System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers; +using Windows.UI; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Helper Class for creating Win2d objects. + /// + public static class CanvasPathGeometry + { + /// + /// Parses the Path data string and converts it to CanvasGeometry. + /// + /// Path data + /// + public static CanvasGeometry CreateGeometry(string pathData) + { + return CreateGeometry(null, pathData); + } + + /// + /// Parses the Path data string and converts it to CanvasGeometry. + /// + /// + /// Path data + /// + public static CanvasGeometry CreateGeometry(ICanvasResourceCreator resourceCreator, string pathData) + { + using (new CultureShield("en-US")) + { + // Get the CanvasGeometry from the path data + return CanvasGeometryParser.Parse(resourceCreator, pathData); + } + } + + /// + /// Creates a Squircle geometry with the specified extents. + /// + /// Resource creator + /// X offset of the TopLeft corner of the Squircle + /// Y offset of the TopLeft corner of the Squircle + /// Width of the Squircle + /// Height of the Squircle + /// Corner Radius on the x-axis + /// Corner Radius on the y-axis + /// + public static CanvasGeometry CreateSquircle(ICanvasResourceCreator resourceCreator, float x, float y, float width, float height, float radiusX, float radiusY) + { + using var pathBuilder = new CanvasPathBuilder(resourceCreator); + pathBuilder.AddSquircleFigure(x, y, width, height, radiusX, radiusY); + return CanvasGeometry.CreatePath(pathBuilder); + } + + /// + /// Parses the given Brush data string and converts it to ICanvasBrush. + /// + /// ICanvasResourceCreator + /// Brush data in string format + /// + public static ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator, string brushData) + { + using (new CultureShield("en-US")) + { + return CanvasBrushParser.Parse(resourceCreator, brushData); + } + } + + /// + /// Parses the given Stroke data string and converts it to ICanvasStroke. + /// + /// ICanvasResourceCreator + /// Stroke data in string format + /// + public static ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator, string strokeData) + { + using (new CultureShield("en-US")) + { + return CanvasStrokeParser.Parse(resourceCreator, strokeData); + } + } + + /// + /// Parses the give CanvasStrokeStyle data string and converts it to CanvasStrokeStyle. + /// + /// CanvasStrokeStyle data in string format + /// object + public static CanvasStrokeStyle CreateStrokeStyle(string styleData) + { + using (new CultureShield("en-US")) + { + return CanvasStrokeStyleParser.Parse(styleData); + } + } + + /// + /// Converts the color string in Hexadecimal or HDR color format to the corresponding Color object. + /// The hexadecimal color string should be in #RRGGBB or #AARRGGBB format. + /// The '#' character is optional. + /// The HDR color string should be in R G B A format. + /// (R, G, B & A should have value in the range between 0 and 1, inclusive) + /// + /// Color string in Hexadecimal or HDR format + /// Color + public static Color CreateColor(string colorString) + { + using (new CultureShield("en-US")) + { + return ColorParser.Parse(colorString); + } + } + + /// + /// Converts a Vector4 High Dynamic Range Color to Color object. + /// Negative components of the Vector4 will be sanitized by taking the absolute + /// value of the component. The HDR Color components should have value in + /// the range between 0 and 1, inclusive. If they are more than 1, they + /// will be clamped at 1. + /// Vector4's X, Y, Z, W components match to Color's R, G, B, A components respectively. + /// + /// High Dynamic Range Color + /// Color + public static Color CreateColor(Vector4 hdrColor) + { + using (new CultureShield("en-US")) + { + return ColorParser.Parse(hdrColor); + } + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasStroke.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasStroke.cs new file mode 100644 index 00000000000..5daf59081c1 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasStroke.cs @@ -0,0 +1,112 @@ +// 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.Brushes; +using Microsoft.Graphics.Canvas.Geometry; +using Windows.UI; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Class to represent the Stroke which can be used to render an outline on a + /// + public sealed class CanvasStroke : ICanvasStroke + { + /// + /// Gets or sets the brush with which the stroke will be rendered + /// + public ICanvasBrush Brush { get; set; } + + /// + /// Gets or sets the width of the + /// + public float Width { get; set; } + + /// + /// Gets or sets the Style of the + /// + public CanvasStrokeStyle Style { get; set; } + + /// + /// Gets or sets the Transform matrix of the brush. + /// + public Matrix3x2 Transform + { + get => GetTransform(); + + set => SetTransform(value); + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush with which the will be rendered + /// Width of the + public CanvasStroke(ICanvasBrush brush, float strokeWidth = 1f) + : this(brush, strokeWidth, new CanvasStrokeStyle()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush with which the will be rendered + /// Width of the + /// Style of the + public CanvasStroke(ICanvasBrush brush, float strokeWidth, CanvasStrokeStyle strokeStyle) + { + Brush = brush; + Width = strokeWidth; + Style = strokeStyle; + } + + /// + /// Initializes a new instance of the class. + /// + /// ICanvasResourceCreator + /// Color of the + /// Width of the + public CanvasStroke(ICanvasResourceCreator device, Color strokeColor, float strokeWidth = 1f) + : this(device, strokeColor, strokeWidth, new CanvasStrokeStyle()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// ICanvasResourceCreator + /// Color of the + /// Width of the + /// Style of the + public CanvasStroke(ICanvasResourceCreator device, Color strokeColor, float strokeWidth, CanvasStrokeStyle strokeStyle) + { + Brush = new CanvasSolidColorBrush(device, strokeColor); + Width = strokeWidth; + Style = strokeStyle; + } + + /// + /// Sets the 's Transform. + /// + /// Transform matrix to set + private void SetTransform(Matrix3x2 value) + { + if (Brush != null) + { + Brush.Transform = value; + } + } + + /// + /// Gets the 's Transform. If stroke is null, then returns Matrix3x2.Identity. + /// + /// Transform matrix of the + private Matrix3x2 GetTransform() + { + return Brush?.Transform ?? Matrix3x2.Identity; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CompositorGeometryExtensions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CompositorGeometryExtensions.cs new file mode 100644 index 00000000000..1d7c863dab6 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CompositorGeometryExtensions.cs @@ -0,0 +1,96 @@ +// 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.Geometry; +using Windows.UI.Composition; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Extension methods for compositor to support Win2d Path Mini Language. + /// + public static class CompositorGeometryExtensions + { + /// + /// Creates a based on the specified path data. + /// + /// + /// Path data (Win2d Path Mini Language) in string format. + /// + public static CompositionPath CreatePath(this Compositor compositor, string pathData) + { + // Create CanvasGeometry + var geometry = CanvasPathGeometry.CreateGeometry(pathData); + + // Create CompositionPath + return new CompositionPath(geometry); + } + + /// + /// Creates a based on the given path data. + /// + /// + /// Path data (Win2d Path Mini Language) in string format. + /// + public static CompositionPathGeometry CreatePathGeometry(this Compositor compositor, string pathData) + { + // Create CanvasGeometry + var geometry = CanvasPathGeometry.CreateGeometry(pathData); + + // Create CompositionPathGeometry + return compositor.CreatePathGeometry(new CompositionPath(geometry)); + } + + /// + /// Creates a based on the specified path data. + /// + /// + /// Path data (Win2d Path Mini Language) in string format. + /// + public static CompositionSpriteShape CreateSpriteShape(this Compositor compositor, string pathData) + { + // Create CanvasGeometry + var geometry = CanvasPathGeometry.CreateGeometry(pathData); + + // Create CompositionPathGeometry + var pathGeometry = compositor.CreatePathGeometry(new CompositionPath(geometry)); + + // Create CompositionSpriteShape + return compositor.CreateSpriteShape(pathGeometry); + } + + /// + /// Creates a from the specified . + /// + /// + /// + /// CompositionGeometricClip + public static CompositionGeometricClip CreateGeometricClip(this Compositor compositor, CanvasGeometry geometry) + { + // Create the CompositionPath + var path = new CompositionPath(geometry); + + // Create the CompositionPathGeometry + var pathGeometry = compositor.CreatePathGeometry(path); + + // Create the CompositionGeometricClip + return compositor.CreateGeometricClip(pathGeometry); + } + + /// + /// Parses the specified path data and converts it to . + /// + /// + /// Path data (Win2d Path Mini Language) in string format. + /// + public static CompositionGeometricClip CreateGeometricClip(this Compositor compositor, string pathData) + { + // Create the CanvasGeometry from the path data + var geometry = CanvasPathGeometry.CreateGeometry(pathData); + + // Create the CompositionGeometricClip + return compositor.CreateGeometricClip(geometry); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/CanvasRoundRect.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/CanvasRoundRect.cs new file mode 100644 index 00000000000..103ec3628bb --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/CanvasRoundRect.cs @@ -0,0 +1,280 @@ +// 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; +using System.Numerics; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core +{ + /// + /// Structure which encapsulates the details of each of the core points of the path of the rounded rectangle which is calculated based on + /// either the given (Size, CornerRadius, BorderThickness and Padding) or (Size, RadiusX and RadiusY). + /// + internal struct CanvasRoundRect + { + private const float Factor = 0.5f; + + private readonly float _leftTopWidth; + private readonly float _topLeftHeight; + private readonly float _topRightHeight; + private readonly float _rightTopWidth; + private readonly float _rightBottomWidth; + private readonly float _bottomRightHeight; + private readonly float _bottomLeftHeight; + private readonly float _leftBottomWidth; + + // This is the location of the properties within the Rect + // |--LeftTop----------------------RightTop--| + // | | + // TopLeft TopRight + // | | + // | | + // | | + // | | + // | | + // | | + // BottomLeft BottomRight + // | | + // |--LeftBottom----------------RightBottom--| + internal float LeftTopX { get; private set; } + + internal float LeftTopY { get; private set; } + + internal float TopLeftX { get; private set; } + + internal float TopLeftY { get; private set; } + + internal float TopRightX { get; private set; } + + internal float TopRightY { get; private set; } + + internal float RightTopX { get; private set; } + + internal float RightTopY { get; private set; } + + internal float RightBottomX { get; private set; } + + internal float RightBottomY { get; private set; } + + internal float BottomRightX { get; private set; } + + internal float BottomRightY { get; private set; } + + internal float BottomLeftX { get; private set; } + + internal float BottomLeftY { get; private set; } + + internal float LeftBottomX { get; private set; } + + internal float LeftBottomY { get; private set; } + + internal float Width { get; } + + internal float Height { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Origin of the Rect (absolute location of Top Left corner) + /// Size of the Rect + /// CornerRadius + /// BorderThickness + /// Padding + /// Flag to indicate whether outer or inner border needs + /// to be calculated + internal CanvasRoundRect(Vector2 origin, Vector2 size, Vector4 cornerRadius, Vector4 borderThickness, Vector4 padding, bool isOuterBorder) + : this() + { + Width = Math.Max(0f, size.X); + Height = Math.Max(0f, size.Y); + + var left = Factor * (borderThickness.X + padding.X); + var top = Factor * (borderThickness.Y + padding.Y); + var right = Factor * (borderThickness.Z + padding.Z); + var bottom = Factor * (borderThickness.W + padding.W); + + if (isOuterBorder) + { + // Top Left corner radius + if (cornerRadius.X.IsZero()) + { + _leftTopWidth = _topLeftHeight = 0f; + } + else + { + _leftTopWidth = cornerRadius.X + left; + _topLeftHeight = cornerRadius.X + top; + } + + // Top Right corner radius + if (cornerRadius.Y.IsZero()) + { + _topRightHeight = _rightTopWidth = 0f; + } + else + { + _topRightHeight = cornerRadius.Y + top; + _rightTopWidth = cornerRadius.Y + right; + } + + // Bottom Right corner radius + if (cornerRadius.Z.IsZero()) + { + _rightBottomWidth = _bottomRightHeight = 0f; + } + else + { + _rightBottomWidth = cornerRadius.Z + right; + _bottomRightHeight = cornerRadius.Z + bottom; + } + + // Bottom Left corner radius + if (cornerRadius.W.IsZero()) + { + _bottomLeftHeight = _leftBottomWidth = 0f; + } + else + { + _bottomLeftHeight = cornerRadius.W + bottom; + _leftBottomWidth = cornerRadius.W + left; + } + } + else + { + _leftTopWidth = Math.Max(0f, cornerRadius.X - left); + _topLeftHeight = Math.Max(0f, cornerRadius.X - top); + _topRightHeight = Math.Max(0f, cornerRadius.Y - top); + _rightTopWidth = Math.Max(0f, cornerRadius.Y - right); + _rightBottomWidth = Math.Max(0f, cornerRadius.Z - right); + _bottomRightHeight = Math.Max(0f, cornerRadius.Z - bottom); + _bottomLeftHeight = Math.Max(0f, cornerRadius.W - bottom); + _leftBottomWidth = Math.Max(0f, cornerRadius.W - left); + } + + // Calculate the anchor points + ComputeCoordinates(origin.X, origin.Y); + } + + /// + /// Initializes a new instance of the struct. + /// + /// Top Left corner of the Rounded Rectangle + /// Dimensions of the Rounded Rectangle + /// Radius of the corners on the x-axis + /// Radius of the corners on the y-axis + internal CanvasRoundRect(Vector2 origin, Vector2 size, float radiusX, float radiusY) + : this(origin.X, origin.Y, size.X, size.Y, radiusX, radiusY) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// X offset of the Top Left corner of the Rounded Rectangle + /// Y offset of the Top Left corner of the Rounded Rectangle + /// Width of the Rounded Rectangle. + /// Height of the Rounded Rectangle. + /// Radius of the corners on the x-axis + /// Radius of the corners on the y-axis + internal CanvasRoundRect(float x, float y, float width, float height, float radiusX, float radiusY) + : this() + { + Width = Math.Max(0f, width); + Height = Math.Max(0f, height); + + // Sanitize the radii by taking the absolute value + radiusX = Math.Min(Math.Abs(radiusX), width / 2f); + radiusY = Math.Min(Math.Abs(radiusY), height / 2); + + _leftTopWidth = radiusX; + _rightTopWidth = radiusX; + _rightBottomWidth = radiusX; + _leftBottomWidth = radiusX; + _topLeftHeight = radiusY; + _topRightHeight = radiusY; + _bottomRightHeight = radiusY; + _bottomLeftHeight = radiusY; + + ComputeCoordinates(x, y); + } + + /// + /// Computes the coordinates of the crucial points on the CanvasRoundRect + /// + /// X coordinate of the origin. + /// Y coordinate of the origin. + private void ComputeCoordinates(float originX, float originY) + { + // compute the coordinates of the key points + var leftTopX = _leftTopWidth; + var leftTopY = 0f; + var rightTopX = Width - _rightTopWidth; + var rightTopY = 0f; + var topRightX = Width; + var topRightY = _topRightHeight; + var bottomRightX = Width; + var bottomRightY = Height - _bottomRightHeight; + var rightBottomX = Width - _rightBottomWidth; + var rightBottomY = Height; + var leftBottomX = _leftBottomWidth; + var leftBottomY = Height; + var bottomLeftX = 0f; + var bottomLeftY = Height - _bottomLeftHeight; + var topLeftX = 0f; + var topLeftY = _topLeftHeight; + + // check anchors for overlap and resolve by partitioning corners according to + // the percentage of each one. + // top edge + if (leftTopX > rightTopX) + { + var v = _leftTopWidth / (_leftTopWidth + _rightTopWidth) * Width; + leftTopX = v; + rightTopX = v; + } + + // right edge + if (topRightY > bottomRightY) + { + var v = _topRightHeight / (_topRightHeight + _bottomRightHeight) * Height; + topRightY = v; + bottomRightY = v; + } + + // bottom edge + if (leftBottomX > rightBottomX) + { + var v = _leftBottomWidth / (_leftBottomWidth + _rightBottomWidth) * Width; + rightBottomX = v; + leftBottomX = v; + } + + // left edge + if (topLeftY > bottomLeftY) + { + var v = _topLeftHeight / (_topLeftHeight + _bottomLeftHeight) * Height; + bottomLeftY = v; + topLeftY = v; + } + + // Apply origin translation + LeftTopX = leftTopX + originX; + LeftTopY = leftTopY + originY; + RightTopX = rightTopX + originX; + RightTopY = rightTopY + originY; + TopRightX = topRightX + originX; + TopRightY = topRightY + originY; + BottomRightX = bottomRightX + originX; + BottomRightY = bottomRightY + originY; + RightBottomX = rightBottomX + originX; + RightBottomY = rightBottomY + originY; + LeftBottomX = leftBottomX + originX; + LeftBottomY = leftBottomY + originY; + BottomLeftX = bottomLeftX + originX; + BottomLeftY = bottomLeftY + originY; + TopLeftX = topLeftX + originX; + TopLeftY = topLeftY + originY; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/GeometryTypeDefinitions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/GeometryTypeDefinitions.cs new file mode 100644 index 00000000000..027000a6547 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/GeometryTypeDefinitions.cs @@ -0,0 +1,59 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core +{ + /// + /// Enum for the various PathFigures. + /// + internal enum PathFigureType + { + FillRule, + PathFigure, + EllipseFigure, + PolygonFigure, + RectangleFigure, + RoundedRectangleFigure + } + + /// + /// Enum for the various PathElements. + /// + internal enum PathElementType + { + MoveTo, + Line, + HorizontalLine, + VerticalLine, + QuadraticBezier, + SmoothQuadraticBezier, + CubicBezier, + SmoothCubicBezier, + Arc, + ClosePath + } + + /// + /// Enum for the various types of Brushes. + /// + internal enum BrushType + { + SolidColor, + LinearGradient, + RadialGradient, + LinearGradientHdr, + RadialGradientHdr + } + + /// + /// Enum for the various types of GradientStop attributes. + /// + internal enum GradientStopAttributeType + { + Main, + Additional, + MainHdr, + AdditionalHdr + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/PathElementFactory.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/PathElementFactory.cs new file mode 100644 index 00000000000..ee77de7b668 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/PathElementFactory.cs @@ -0,0 +1,157 @@ +// 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; +using System.Text.RegularExpressions; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core +{ + /// + /// Factory class to instantiate various PathElements. + /// + internal static class PathElementFactory + { + /// + /// Creates a default Path Element for the given PathFigureType. + /// + /// PathFigureType + /// ICanvasPathElement + internal static ICanvasPathElement CreateDefaultPathElement(PathFigureType figureType) + { + if (figureType == PathFigureType.FillRule) + { + return new FillRuleElement(); + } + + static ICanvasPathElement Throw() => throw new ArgumentException("Creation of Only Default FillRuleElement is supported."); + + return Throw(); + } + + /// + /// Creates a default Path Element for the given PathElementType. + /// + /// PathElementType + /// ICanvasPathElement + internal static ICanvasPathElement CreateDefaultPathElement(PathElementType elementType) + { + if (elementType == PathElementType.ClosePath) + { + return new ClosePathElement(); + } + + static ICanvasPathElement Throw() => throw new ArgumentException("Creation of Only Default ClosePathElement is supported."); + + return Throw(); + } + + /// + /// Instantiates a PathElement based on the PathFigureType. + /// + /// PathFigureType + /// Match object + /// Index of the path element in the Path data + /// ICanvasPathElement + internal static ICanvasPathElement CreatePathFigure(PathFigureType figureType, Match match, int index) + { + var element = CreatePathElement(figureType); + element?.Initialize(match, index); + return element; + } + + /// + /// Instantiates a PathElement based on the PathFigureType. + /// + /// PathFigureType + /// Capture object + /// Index of the capture + /// Indicates whether the coordinates are absolute or relative + /// ICanvasPathElement + internal static ICanvasPathElement CreateAdditionalPathFigure(PathFigureType figureType, Capture capture, int index, bool isRelative) + { + var element = CreatePathElement(figureType); + element?.InitializeAdditional(capture, index, isRelative); + return element; + } + + /// + /// Instantiates a PathElement based on the PathElementType. + /// + /// PathElementType + /// Match object + /// Index of the path element in the Path data + /// ICanvasPathElement + internal static ICanvasPathElement CreatePathElement(PathElementType elementType, Match match, int index) + { + var element = CreatePathElement(elementType); + element?.Initialize(match, index); + return element; + } + + /// + /// Instantiates a PathElement based on the PathElementType. + /// + /// PathElementType + /// Capture object + /// Index of the capture + /// Indicates whether the coordinates are absolute or relative + /// ICanvasPathElement + internal static ICanvasPathElement CreateAdditionalPathElement(PathElementType elementType, Capture capture, int index, bool isRelative) + { + // Additional attributes in MoveTo Command must be converted + // to Line commands + if (elementType == PathElementType.MoveTo) + { + elementType = PathElementType.Line; + } + + var element = CreatePathElement(elementType); + element?.InitializeAdditional(capture, index, isRelative); + return element; + } + + /// + /// Instantiates a PathElement based on the PathFigureType. + /// + /// PathFigureType + /// ICanvasPathElement + private static ICanvasPathElement CreatePathElement(PathFigureType figureType) + { + return figureType switch + { + PathFigureType.FillRule => new FillRuleElement(), + PathFigureType.PathFigure => new CanvasPathFigure(), + PathFigureType.EllipseFigure => new CanvasEllipseFigure(), + PathFigureType.PolygonFigure => new CanvasPolygonFigure(), + PathFigureType.RectangleFigure => new CanvasRectangleFigure(), + PathFigureType.RoundedRectangleFigure => new CanvasRoundRectangleFigure(), + _ => throw new ArgumentOutOfRangeException(nameof(figureType), figureType, "Invalid PathFigureType!") + }; + } + + /// + /// Instantiates a PathElement based on the PathElementType. + /// + /// PathElementType + /// ICanvasPathElement + private static ICanvasPathElement CreatePathElement(PathElementType elementType) + { + return elementType switch + { + PathElementType.MoveTo => new MoveToElement(), + PathElementType.Line => new LineElement(), + PathElementType.HorizontalLine => new HorizontalLineElement(), + PathElementType.VerticalLine => new VerticalLineElement(), + PathElementType.QuadraticBezier => new QuadraticBezierElement(), + PathElementType.SmoothQuadraticBezier => new SmoothQuadraticBezierElement(), + PathElementType.CubicBezier => new CubicBezierElement(), + PathElementType.SmoothCubicBezier => new SmoothCubicBezierElement(), + PathElementType.Arc => new ArcElement(), + PathElementType.ClosePath => new ClosePathElement(), + _ => throw new ArgumentOutOfRangeException(nameof(elementType), elementType, "Invalid PathElementType!") + }; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/RegexFactory.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/RegexFactory.cs new file mode 100644 index 00000000000..79112c46df4 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/RegexFactory.cs @@ -0,0 +1,602 @@ +// 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.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +[assembly: InternalsVisibleTo("UnitTests.UWP")] + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core +{ + /// + /// Contains all the Regular Expressions which are used for parsing the Win2d Path Mini Language. + /// + internal static class RegexFactory + { + // Whitespace + private const string Spacer = @"\s*"; + + // Whitespace or comma + private const string SpaceOrComma = @"(?:\s+|\s*,\s*)"; + + // Whitespace or comma or a minus/plus sign (look ahead) + private const string Sep = @"(?:\s+|\s*,\s*|(?=[-+.]))"; + + // Whitespace or comma or a '#' sign (look ahead) + private const string ColorSep = @"(?:\s+|\s*,\s*|(?=[#]))"; + + // Positive Integer + private const string Integer = @"[+-]?[0-9]+"; + + // Positive Integer + private const string PositiveInteger = @"[+]?[0-9]+"; + + // Floating point number + private const string Float = @"(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)"; + + // Positive Floating point number + private const string PositiveFloat = @"(?:[+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)"; + + // Floating point number between 0 and 1, inclusive + // private const string Float01 = @"(?:(?[Mm]{Spacer}{Pos}(?:{Sep}{Pos})*{Spacer})"; + + // Line + private static readonly string Line = $"(?[Ll]{Spacer}{Pos}(?:{Sep}{Pos})*{Spacer})"; + + // Horizontal Line + private static readonly string HorizontalLine = $"(?[Hh]{Spacer}{Float}(?:{Sep}{Float})*{Spacer})"; + + // Vertical Line + private static readonly string VerticalLine = $"(?[Vv]{Spacer}{Float}(?:{Sep}{Float})*{Spacer})"; + + // Quadratic Bezier + private static readonly string QuadraticBezier = $"(?[Qq]{Spacer}{Pos}{Sep}{Pos}(?:{Sep}{Pos}{Sep}{Pos})*{Spacer})"; + + // Smooth Quadratic Bezier + private static readonly string SmoothQuadraticBezier = $"(?[Tt]{Spacer}{Pos}(?:{Sep}{Pos})*{Spacer})"; + + // Cubic Bezier + private static readonly string CubicBezier = $"(?[Cc]{Spacer}{Pos}{Sep}{Pos}{Sep}{Pos}(?:{Sep}{Pos}{Sep}{Pos}{Sep}{Pos})*{Spacer})"; + + // Smooth Cubic Bezier + private static readonly string SmoothCubicBezier = $"(?[Ss]{Spacer}{Pos}{Sep}{Pos}(?:{Sep}{Pos}{Sep}{Pos})*{Spacer})"; + + // Arc + private static readonly string Arc = $"(?[Aa]{Spacer}{Float}{Sep}{Float}{Sep}{Float}{SpaceOrComma}[01]{SpaceOrComma}[01]{Sep}{Pos}" + + $"(?:{Sep}{Float}{Sep}{Float}{Sep}{Float}{SpaceOrComma}[01]{SpaceOrComma}[01]{Sep}{Pos})*{Spacer})"; + + // Close Path + private static readonly string ClosePath = $"(?[Zz]{Spacer})"; + + // CanvasPathFigure + private static readonly string CanvasPathFigureRegexString = + $"{MoveTo}" + // M x,y + "(" + + $"{Line}+|" + // L x,y + $"{HorizontalLine}+|" + // H x + $"{VerticalLine}+|" + // V y + $"{QuadraticBezier}+|" + // Q x1,y1 x,y + $"{SmoothQuadraticBezier}+|" + // T x,y + $"{CubicBezier}+|" + // C x1,y1 x2,y2 x,y + $"{SmoothCubicBezier}+|" + // S x2,y2 x,y + $"{Arc}+|" + // A radX, radY, angle, isLargeArc, sweepDirection, x, y + ")+" + + $"{ClosePath}?"; // Close Path (Optional) + + // Fill Rule + private static readonly string FillRule = $"{Spacer}(?[Ff]{Spacer}[01])"; + + // PathFigure + private static readonly string PathFigure = $"{Spacer}(?{CanvasPathFigureRegexString})"; + + // Ellipse Figure + private static readonly string EllipseFigure = $"{Spacer}(?[Oo]{Spacer}{Float}{Sep}{Float}{Sep}{Pos}" + + $"(?:{Sep}{Float}{Sep}{Float}{Sep}{Pos})*)"; + + // Polygon Figure + private static readonly string PolygonFigure = $"{Spacer}(?[Pp]{Spacer}{Integer}{Sep}{Float}{Sep}{Pos}" + + $"(?:{Sep}{Integer}{Sep}{Float}{Sep}{Pos})*)"; + + // Rectangle Figure + private static readonly string RectangleFigure = $"{Spacer}(?[Rr]{Spacer}{Pos}{Sep}{Float}{Sep}{Float}" + + $"(?:{Sep}{Pos}{Sep}{Float}{Sep}{Float})*)"; + + // Rounded Rectangle Figure + private static readonly string RoundedRectangleFigure = $"{Spacer}(?[Uu]{Spacer}{Pos}{Sep}{Float}{Sep}{Float}{Sep}{Float}{Sep}{Float}" + + $"(?:{Sep}{Pos}{Sep}{Float}{Sep}{Float}{Sep}{Float}{Sep}{Float})*)"; + + // CanvasGeometry + private static readonly string CanvasGeometryRegexString = + $"{FillRule}?" + // F0 or F1 + "(" + + $"{PathFigure}+|" + // Path Figure + $"{EllipseFigure}+|" + // O radX, radY, centerX, centerY + $"{PolygonFigure}+|" + // P numSides, radius, centerX, centerY + $"{RectangleFigure}+|" + // R x, y, width, height + $"{RoundedRectangleFigure}+" + // U x, y, width, height, radiusX, radiusY + ")+"; + + // MoveTo + private static readonly string MoveToAttributes = $"(?{Float}){Sep}(?{Float})"; + private static readonly string MoveToRegexString = $"{Spacer}(?
(?[Mm]){Spacer}{MoveToAttributes})" + + $"(?{Sep}{Pos})*"; + + // Line + private static readonly string LineAttributes = $"(?{Float}){Sep}(?{Float})"; + private static readonly string LineRegexString = $"{Spacer}(?
(?[Ll]){Spacer}{LineAttributes})" + + $"(?{Sep}{Pos})*"; + + // Horizontal Line + private static readonly string HorizontalLineAttributes = $"(?{Float})"; + private static readonly string HorizontalLineRegexString = $"{Spacer}(?
(?[Hh]){Spacer}{HorizontalLineAttributes})" + + $"(?{Sep}{Float})*"; + + // Vertical Line + private static readonly string VerticalLineAttributes = $"(?{Float})"; + private static readonly string VerticalLineRegexString = $"{Spacer}(?
(?[Vv]){Spacer}{VerticalLineAttributes})" + + $"(?{Sep}{Float})*"; + + // Quadratic Bezier + private static readonly string QuadraticBezierAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})"; + private static readonly string QuadraticBezierRegexString = $"{Spacer}(?
(?[Qq]){Spacer}{QuadraticBezierAttributes})" + + $"(?{Sep}{Pos}{Sep}{Pos})*"; + + // Smooth Quadratic Bezier + private static readonly string SmoothQuadraticBezierAttributes = $"(?{Float}){Sep}(?{Float})"; + private static readonly string SmoothQuadraticBezierRegexString = $"{Spacer}(?
(?[Tt]){Spacer}{SmoothQuadraticBezierAttributes})" + + $"(?{Sep}{Pos})*"; + + // Cubic Bezier + private static readonly string CubicBezierAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}" + + $"(?{Float}){Sep}(?{Float})"; + + private static readonly string CubicBezierRegexString = $"{Spacer}(?
(?[Cc]){Spacer}{CubicBezierAttributes})" + + $"(?{Sep}{Pos}{Sep}{Pos}{Sep}{Pos})*"; + + // Smooth Cubic Bezier + private static readonly string SmoothCubicBezierAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})"; + private static readonly string SmoothCubicBezierRegexString = $"{Spacer}(?
(?[Ss]){Spacer}{SmoothCubicBezierAttributes})" + + $"(?{Sep}{Pos}{Sep}{Pos})*"; + + // Arc + private static readonly string ArcAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){SpaceOrComma}" + + $"(?[01]){SpaceOrComma}(?[01]){Sep}(?{Float}){Sep}(?{Float})"; + + private static readonly string ArcRegexString = $"{Spacer}(?
(?[Aa]){Spacer}{ArcAttributes})" + + $"(?{Sep}{Float}{Sep}{Float}{Sep}{Float}{SpaceOrComma}[01]{SpaceOrComma}[01]{Sep}{Pos})*"; + + // Close Path + private static readonly string ClosePathRegexString = $"{Spacer}(?
(?[Zz])){Spacer}"; + + // Fill Rule + private static readonly string FillRuleRegexString = $"{Spacer}(?
(?[Ff]){Spacer}(?[01]))"; + + // Path Figure + private static readonly string PathFigureRegexString = $"{Spacer}(?
{PathFigure})"; + + // Ellipse Figure + private static readonly string EllipseFigureAttributes = $"(?{Float}){Sep}(?{Float}){Sep}" + + $"(?{Float}){Sep}(?{Float})"; + + private static readonly string EllipseFigureRegexString = $"{Spacer}(?
(?[Oo]){Spacer}{EllipseFigureAttributes})" + + $"(?{Sep}{Float}{Sep}{Float}{Sep}{Pos})*"; + + // Polygon Figure + private static readonly string PolygonFigureAttributes = $"(?{Integer}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})"; + private static readonly string PolygonFigureRegexString = $"{Spacer}(?
(?[Pp]){Spacer}{PolygonFigureAttributes})" + + $"(?{Sep}{Integer}{Sep}{Float}{Sep}{Pos})*"; + + // Rectangle Figure + private static readonly string RectangleFigureAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})"; + private static readonly string RectangleFigureRegexString = $"{Spacer}(?
(?[Rr]){Spacer}{RectangleFigureAttributes})" + + $"(?{Sep}{Pos}{Sep}{Float}{Sep}{Float})*"; + + // Rectangle Figure + private static readonly string RoundedRectangleFigureAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})" + + $"{Sep}(?{Float}){Sep}(?{Float})"; + + private static readonly string RoundedRectangleFigureRegexString = $"{Spacer}(?
(?[Uu]){Spacer}{RoundedRectangleFigureAttributes})" + + $"(?{Sep}{Pos}{Sep}{Float}{Sep}{Float}{Sep}{Float}{Sep}{Float})*"; + + // ARGB Color + private static readonly string HexColor = $"(?:#?(?:{Hex}{{2}})?{Hex}{{6}})"; + + // Alpha + private static readonly string Alpha = $"(?{Hex}{{2}})"; + + // Red + private static readonly string Red = $"(?{Hex}{{2}})"; + + // Green + private static readonly string Green = $"(?{Hex}{{2}})"; + + // Blue + private static readonly string Blue = $"(?{Hex}{{2}})"; + + // Hexadecimal Color + private static readonly string RgbColor = $"(?{HexColor})"; + + // HDR Color (Vector4 in which each component has a value between 0 and 1, inclusive) + private static readonly string HdrColor = $"(?{Float01}{Sep}{Float01}{Sep}{Float01}{Sep}{Float01})"; + + // Hexadecimal Color Attributes + private static readonly string RgbColorAttributes = $"(?#{{0,1}}{Alpha}{{0,1}}{Red}{Green}{Blue})"; + + // HDR Color Attributes (Vector4 in which each component has a value between 0 and 1, inclusive) + private static readonly string HdrColorAttributes = $"(?(?{Float01}){Sep}(?{Float01}){Sep}(?{Float01}){Sep}(?{Float01}))"; + + private static readonly string ColorRegexString = $"(?:{RgbColorAttributes}|{HdrColorAttributes})"; + + // Start Point + private static readonly string StartPoint = $"(?[Mm]{Spacer}{Pos}{Spacer})"; + + // End Point + private static readonly string EndPoint = $"(?[Zz]{Spacer}{Pos}{Spacer})"; + + // Opacity + private static readonly string Opacity = $"(?[Oo]{Spacer}{Float01}{Spacer})"; + + // Alpha Mode + private static readonly string AlphaMode = $"(?[Aa]{Spacer}[012]{Spacer})"; + + // Buffer Precision + private static readonly string BufferPrecision = $"(?[Bb]{Spacer}[01234]{Spacer})"; + + // Edge Behavior + private static readonly string EdgeBehavior = $"(?[Ee]{Spacer}[012]{Spacer})"; + + // PreInterpolation Color Space + private static readonly string PreColorSpace = $"(?[Pp]{Spacer}[012]{Spacer})"; + + // PostInterpolation Color Space + private static readonly string PostColorSpace = $"(?[Rr]{Spacer}[012]{Spacer})"; + + // Radius in X-axis + private static readonly string RadiusX = $"(?{Float})"; + + // Radius in Y-axis + private static readonly string RadiusY = $"(?{Float})"; + + // Center location on X-axis + private static readonly string CenterX = $"(?{Float})"; + + // Center location on Y-axis + private static readonly string CenterY = $"(?{Float})"; + + // Origin Offset + private static readonly string OriginOffset = $"(?[Ff]{Spacer}{Pos}{Spacer})"; + + // GradientStops + private static readonly string GradientStops = $"(?[Ss]{Spacer}{Float01}{ColorSep}{HexColor}(?:{Sep}{Float01}{ColorSep}{HexColor})*{Spacer})"; + + // GradientStopHdrs + private static readonly string GradientStopHdrs = $"(?[Ss]{Spacer}{Float01}{Sep}{HdrColor}(?:{Sep}{Float01}{Sep}{HdrColor})*{Spacer})"; + + // Solid Color Brush + private static readonly string SolidColorBrush = $"(?[Ss][Cc]{Spacer}(?:{RgbColor}|{HdrColor}){Spacer}{Opacity}?)"; + + // LinearGradient + private static readonly string LinearGradient = $"(?[Ll][Gg]{Spacer}{StartPoint}{EndPoint}" + + $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{PreColorSpace}?{PostColorSpace}?" + + $"{GradientStops}+{Spacer})"; + + // RadialGradient + private static readonly string RadialGradient = $"(?[Rr][Gg]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" + + $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{OriginOffset}?{PreColorSpace}?{PostColorSpace}?" + + $"{GradientStops}+{Spacer})"; + + // LinearGradientHdr + private static readonly string LinearGradientHdr = $"(?[Ll][Hh]{Spacer}{StartPoint}{EndPoint}" + + $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{PreColorSpace}?{PostColorSpace}?" + + $"{GradientStopHdrs}+{Spacer})"; + + // RadialGradientHdr + private static readonly string RadialGradientHdr = $"(?[Rr][Hh]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" + + $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{OriginOffset}?{PreColorSpace}?{PostColorSpace}?" + + $"{GradientStopHdrs}+{Spacer})"; + + // Regex for the CanvasBrush + private static readonly string CanvasBrushRegexString = $"(?{SolidColorBrush}|{LinearGradient}|{RadialGradient}|{LinearGradientHdr}|{RadialGradientHdr})"; + + // Start Point + private static readonly string StartPointAttr = $"(?:[Mm]{Spacer}(?{Float}){Sep}(?{Float}){Spacer})"; + + // End Point + private static readonly string EndPointAttr = $"(?:[Zz]{Spacer}(?{Float}){Sep}(?{Float}){Spacer})"; + + // Opacity + private static readonly string OpacityAttr = $"(?:[Oo]{Spacer}(?{Float01}){Spacer})"; + + // Alpha Mode + private static readonly string AlphaModeAttr = $"(?:[Aa]{Spacer}(?[012]){Spacer})"; + + // Buffer Precision + private static readonly string BufferPrecisionAttr = $"(?:[Bb]{Spacer}(?[01234]){Spacer})"; + + // Edge Behavior + private static readonly string EdgeBehaviorAttr = $"(?:[Ee]{Spacer}(?[012]){Spacer})"; + + // PreInterpolation Color Space + private static readonly string PreColorSpaceAttr = $"(?:[Pp]{Spacer}(?[012]){Spacer})"; + + // PostInterpolation Color Space + private static readonly string PostColorSpaceAttr = $"(?:[Rr]{Spacer}(?[012]){Spacer})"; + + // Origin Offset + private static readonly string OriginOffsetAttr = $"(?[Ff]{Spacer}(?{Float}){Sep}(?{Float}){Spacer})"; + + // GradientStop Attributes + private static readonly string GradientStopAttributes = $"(?{Float01}){ColorSep}{RgbColorAttributes}"; + private static readonly string GradientStopMainAttributes = $"(?
[Ss]{Spacer}{GradientStopAttributes})"; + private static readonly string GradientStopRegexString = $"(?{GradientStopMainAttributes}" + $"(?{Sep}{Float01}{ColorSep}{HexColor})*{Spacer})"; + + // GradientStopHdr Attributes + private static readonly string GradientStopHdrAttributes = $"(?{Float01}){Sep}{HdrColorAttributes}"; + private static readonly string GradientStopHdrMainAttributes = $"(?
(?[Ss]){Spacer}{GradientStopHdrAttributes})"; + private static readonly string GradientStopHdrRegexString = $"(?{GradientStopHdrMainAttributes}" + $"(?{Sep}{Float01}{Sep}{HdrColor})*{Spacer})"; + + // Regex for SolidColorBrush Attributes + private static readonly string SolidColorBrushRegexString = $"(?:[Ss][Cc]{Spacer}(?:{RgbColorAttributes}|{HdrColorAttributes}){Spacer}{OpacityAttr}?)"; + + // Regex for LinearGradient Attributes + private static readonly string LinearGradientRegexString = $"[Ll][Gg]{Spacer}{StartPointAttr}{EndPointAttr}" + + $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?" + + $"{EdgeBehaviorAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" + + $"{GradientStops}+{Spacer}"; + + // Regex for RadialGradient Attributes + private static readonly string RadialGradientRegexString = $"[Rr][Gg]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" + + $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?{EdgeBehaviorAttr}?" + + $"{OriginOffsetAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" + + $"{GradientStops}+{Spacer}"; + + // Regex for LinearGradientHdr Attributes + private static readonly string LinearGradientHdrRegexString = $"[Ll][Hh]{Spacer}{StartPointAttr}{EndPointAttr}" + + $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?" + + $"{EdgeBehaviorAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" + + $"{GradientStopHdrs}+{Spacer}"; + + // Regex for RadialGradientHdr Attributes + private static readonly string RadialGradientHdrRegexString = $"[Rr][Hh]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" + + $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?{EdgeBehaviorAttr}?" + + $"{OriginOffsetAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" + + $"{GradientStopHdrs}+{Spacer}"; + + // CanvasStrokeStyle attributes + private static readonly string DashStyle = $"(?:[Dd][Ss]{Spacer}(?[01234]){Spacer})"; + private static readonly string LineJoin = $"(?:[Ll][Jj]{Spacer}(?[0123]){Spacer})"; + private static readonly string MiterLimit = $"(?:[Mm][Ll]{Spacer}(?{Float}){Spacer})"; + private static readonly string DashOffset = $"(?:[Dd][Oo]{Spacer}(?{Float}){Spacer})"; + private static readonly string StartCap = $"(?:[Ss][Cc]{Spacer}(?[0123]){Spacer})"; + private static readonly string EndCap = $"(?:[Ee][Cc]{Spacer}(?[0123]){Spacer})"; + private static readonly string DashCap = $"(?:[Dd][Cc]{Spacer}(?[0123]){Spacer})"; + private static readonly string TransformBehavior = $"(?:[Tt][Bb]{Spacer}(?[012]){Spacer})"; + private static readonly string CustomDashAttribute = $"(?{Float}){Sep}(?{Float})"; + private static readonly string CustomDashStyle = $"(?[Cc][Dd][Ss]{Spacer}(?
{CustomDashAttribute})" + $"(?{Sep}{Float}{Sep}{Float})*{Spacer})"; + + // CanvasStrokeStyle Regex + private static readonly string CanvasStrokeStyleRegexString = $"(?[Cc][Ss][Ss]{Spacer}{DashStyle}?{LineJoin}?{MiterLimit}?{DashOffset}?" + + $"{StartCap}?{EndCap}?{DashCap}?{TransformBehavior}?{CustomDashStyle}?)"; + + // CanvasStroke Regex + private static readonly string CanvasStrokeRegexString = $"(?[Ss][Tt]{Spacer}" + + $"(?{Float}){Spacer}" + + $"{CanvasBrushRegexString}{Spacer}" + + $"{CanvasStrokeStyleRegexString}?)"; + + private static readonly Dictionary PathFigureRegexes; + private static readonly Dictionary PathFigureAttributeRegexes; + private static readonly Dictionary PathElementRegexes; + private static readonly Dictionary PathElementAttributeRegexes; + private static readonly Dictionary BrushRegexes; + private static readonly Dictionary GradientStopAttributeRegexes; + + /// + /// Gets the Regex to perform validation of Path data. + /// + public static Regex ValidationRegex { get; } + + /// + /// Gets the Regex for parsing the CanvasGeometry string. + /// + public static Regex CanvasGeometryRegex { get; } + + /// + /// Gets the Regex for parsing Hexadecimal Color string. + /// + public static Regex ColorRegex { get; } + + /// + /// Gets the Regex for parsing the ICanvasBrush string. + /// + public static Regex CanvasBrushRegex { get; } + + /// + /// Gets the Regex for parsing the GradientStop string. + /// + public static Regex GradientStopRegex { get; } + + /// + /// Gets the Regex for parsing the GradientStopHdr string. + /// + public static Regex GradientStopHdrRegex { get; } + + /// + /// Gets the Regex for parsing the CanvasStrokeStyle string. + /// + public static Regex CanvasStrokeStyleRegex { get; } + + /// + /// Gets the Regex for parsing the CanvasStroke string. + /// + public static Regex CanvasStrokeRegex { get; } + + /// + /// Gets the Regex for parsing the CustomDashStyle attributes. + /// + public static Regex CustomDashAttributeRegex { get; } + + /// + /// Initializes static members of the class. + /// + static RegexFactory() + { + PathFigureRegexes = new Dictionary + { + [PathFigureType.FillRule] = new Regex(FillRuleRegexString, RegexOptions.Compiled), + [PathFigureType.PathFigure] = new Regex(PathFigureRegexString, RegexOptions.Compiled), + [PathFigureType.EllipseFigure] = new Regex(EllipseFigureRegexString, RegexOptions.Compiled), + [PathFigureType.PolygonFigure] = new Regex(PolygonFigureRegexString, RegexOptions.Compiled), + [PathFigureType.RectangleFigure] = new Regex(RectangleFigureRegexString, RegexOptions.Compiled), + [PathFigureType.RoundedRectangleFigure] = new Regex(RoundedRectangleFigureRegexString, RegexOptions.Compiled) + }; + + PathFigureAttributeRegexes = new Dictionary + { + // Not Applicable for FillRuleElement + [PathFigureType.FillRule] = null, + + // Not Applicable for CanvasPathFigure + [PathFigureType.PathFigure] = null, + [PathFigureType.EllipseFigure] = new Regex($"{Sep}{EllipseFigureAttributes}", RegexOptions.Compiled), + [PathFigureType.PolygonFigure] = new Regex($"{Sep}{PolygonFigureAttributes}", RegexOptions.Compiled), + [PathFigureType.RectangleFigure] = new Regex($"{Sep}{RectangleFigureAttributes}", RegexOptions.Compiled), + [PathFigureType.RoundedRectangleFigure] = new Regex($"{Sep}{RoundedRectangleFigureAttributes}", RegexOptions.Compiled) + }; + + PathElementRegexes = new Dictionary + { + [PathElementType.MoveTo] = new Regex(MoveToRegexString, RegexOptions.Compiled), + [PathElementType.Line] = new Regex(LineRegexString, RegexOptions.Compiled), + [PathElementType.HorizontalLine] = new Regex(HorizontalLineRegexString, RegexOptions.Compiled), + [PathElementType.VerticalLine] = new Regex(VerticalLineRegexString, RegexOptions.Compiled), + [PathElementType.QuadraticBezier] = new Regex(QuadraticBezierRegexString, RegexOptions.Compiled), + [PathElementType.SmoothQuadraticBezier] = new Regex(SmoothQuadraticBezierRegexString, RegexOptions.Compiled), + [PathElementType.CubicBezier] = new Regex(CubicBezierRegexString, RegexOptions.Compiled), + [PathElementType.SmoothCubicBezier] = new Regex(SmoothCubicBezierRegexString, RegexOptions.Compiled), + [PathElementType.Arc] = new Regex(ArcRegexString, RegexOptions.Compiled), + [PathElementType.ClosePath] = new Regex(ClosePathRegexString, RegexOptions.Compiled) + }; + + PathElementAttributeRegexes = new Dictionary + { + [PathElementType.MoveTo] = new Regex($"{Sep}{MoveToAttributes}", RegexOptions.Compiled), + [PathElementType.Line] = new Regex($"{Sep}{LineAttributes}", RegexOptions.Compiled), + [PathElementType.HorizontalLine] = new Regex($"{Sep}{HorizontalLineAttributes}", RegexOptions.Compiled), + [PathElementType.VerticalLine] = new Regex($"{Sep}{VerticalLineAttributes}", RegexOptions.Compiled), + [PathElementType.QuadraticBezier] = new Regex($"{Sep}{QuadraticBezierAttributes}", RegexOptions.Compiled), + [PathElementType.SmoothQuadraticBezier] = new Regex($"{Sep}{SmoothQuadraticBezierAttributes}", RegexOptions.Compiled), + [PathElementType.CubicBezier] = new Regex($"{Sep}{CubicBezierAttributes}", RegexOptions.Compiled), + [PathElementType.SmoothCubicBezier] = new Regex($"{Sep}{SmoothCubicBezierAttributes}", RegexOptions.Compiled), + [PathElementType.Arc] = new Regex($"{Sep}{ArcAttributes}", RegexOptions.Compiled), + + // Not Applicable for ClosePathElement as it has no attributes + [PathElementType.ClosePath] = null + }; + + BrushRegexes = new Dictionary + { + [BrushType.SolidColor] = new Regex(SolidColorBrushRegexString, RegexOptions.Compiled), + [BrushType.LinearGradient] = new Regex(LinearGradientRegexString, RegexOptions.Compiled), + [BrushType.LinearGradientHdr] = new Regex(LinearGradientHdrRegexString, RegexOptions.Compiled), + [BrushType.RadialGradient] = new Regex(RadialGradientRegexString, RegexOptions.Compiled), + [BrushType.RadialGradientHdr] = new Regex(RadialGradientHdrRegexString, RegexOptions.Compiled) + }; + + GradientStopAttributeRegexes = new Dictionary + { + [GradientStopAttributeType.Main] = new Regex(GradientStopMainAttributes, RegexOptions.Compiled), + [GradientStopAttributeType.Additional] = new Regex($"{Sep}{GradientStopAttributes}", RegexOptions.Compiled), + [GradientStopAttributeType.MainHdr] = new Regex(GradientStopHdrMainAttributes, RegexOptions.Compiled), + [GradientStopAttributeType.AdditionalHdr] = new Regex($"{Sep}{GradientStopHdrAttributes}", RegexOptions.Compiled) + }; + + ValidationRegex = new Regex(@"\s+"); + CanvasGeometryRegex = new Regex(CanvasGeometryRegexString, RegexOptions.Compiled); + ColorRegex = new Regex(ColorRegexString, RegexOptions.Compiled); + CanvasBrushRegex = new Regex(CanvasBrushRegexString, RegexOptions.Compiled); + GradientStopRegex = new Regex(GradientStopRegexString, RegexOptions.Compiled); + GradientStopHdrRegex = new Regex(GradientStopHdrRegexString, RegexOptions.Compiled); + CanvasStrokeStyleRegex = new Regex(CanvasStrokeStyleRegexString, RegexOptions.Compiled); + CanvasStrokeRegex = new Regex(CanvasStrokeRegexString, RegexOptions.Compiled); + CustomDashAttributeRegex = new Regex($"{Sep}{CustomDashAttribute}", RegexOptions.Compiled); + } + + /// + /// Get the Regex for the given PathFigureType + /// + /// PathFigureType + /// Regex + internal static Regex GetRegex(PathFigureType figureType) + { + return PathFigureRegexes[figureType]; + } + + /// + /// Get the Regex for the given PathElementType + /// + /// PathElementType + /// Regex + internal static Regex GetRegex(PathElementType elementType) + { + return PathElementRegexes[elementType]; + } + + /// + /// Get the Regex for extracting attributes of the given PathFigureType + /// + /// PathFigureType + /// Regex + internal static Regex GetAttributesRegex(PathFigureType figureType) + { + return PathFigureAttributeRegexes[figureType]; + } + + /// + /// Get the Regex for extracting attributes of the given PathElementType + /// + /// PathElementType + /// Regex + internal static Regex GetAttributesRegex(PathElementType elementType) + { + return PathElementAttributeRegexes[elementType]; + } + + /// + /// Gets the Regex for extracting the attributes of the given BrushType + /// + /// BrushType + /// Regex + internal static Regex GetAttributesRegex(BrushType brushType) + { + return BrushRegexes[brushType]; + } + + /// + /// Gets the Regex for extracting the attributes of the given + /// GradientStopAttributeType + /// + /// GradientStopAttributeType + /// Regex + internal static Regex GetAttributesRegex(GradientStopAttributeType gsAttrType) + { + return GradientStopAttributeRegexes[gsAttrType]; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CultureShield.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CultureShield.cs new file mode 100644 index 00000000000..8dc577f7915 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CultureShield.cs @@ -0,0 +1,59 @@ +// 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; +using System.Globalization; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Class which can be used to encapsulate code statement(s) so that they are executed in a specific culture. + /// + /// Usage example: + /// + /// The following code block will be executed using the French culture. + /// + /// using (new CultureShield("fr-FR")) + /// + /// { + /// + /// ... + /// + /// } + /// + internal readonly ref struct CultureShield + { + private readonly CultureInfo _prevCulture; + + /// + /// Initializes a new instance of the struct so that the encapsulated code statement(s) can be executed using the specified culture. + /// + /// Usage example: + /// + /// The following code block will be executed using the French culture. + /// + /// using (new CultureShield("fr-FR")) + /// + /// { + /// + /// ... + /// + /// } + /// + /// The culture in which the encapsulated code statement(s) are to be executed. + internal CultureShield(string culture) + { + _prevCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo(culture); + } + + /// + /// Disposes the CultureShield object. + /// + public void Dispose() + { + CultureInfo.CurrentCulture = _prevCulture; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/AbstractCanvasBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/AbstractCanvasBrushElement.cs new file mode 100644 index 00000000000..dd0e9c1f27f --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/AbstractCanvasBrushElement.cs @@ -0,0 +1,72 @@ +// 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.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush +{ + /// + /// Abstract base class for all Brush Elements + /// + internal abstract class AbstractCanvasBrushElement : ICanvasBrushElement + { +#pragma warning disable SA1401 // Fields should be private + protected float _opacity; +#pragma warning restore SA1401 // Fields should be private + + /// + /// Gets or sets the Brush data defining the Brush Element + /// + public string Data { get; protected set; } + + /// + /// Gets or sets the number of non-whitespace characters in + /// the Brush Data + /// + public int ValidationCount { get; protected set; } + + /// + /// Initializes the Brush Element with the given Capture + /// + /// Capture object + public virtual void Initialize(Capture capture) + { + Data = capture.Value; + + var regex = GetAttributesRegex(); + var match = regex.Match(Data); + if (!match.Success) + { + return; + } + + GetAttributes(match); + + // Get the number of non-whitespace characters in the data + ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length; + } + + /// + /// Creates the ICanvasBrush from the parsed data + /// + /// ICanvasResourceCreator object + /// ICanvasBrush + public abstract ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator); + + /// + /// Gets the Regex for extracting Brush Element Attributes + /// + /// Regex + protected abstract Regex GetAttributesRegex(); + + /// + /// Gets the Brush Element Attributes from the Match + /// + /// Match object + protected abstract void GetAttributes(Match match); + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/ICanvasBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/ICanvasBrushElement.cs new file mode 100644 index 00000000000..0e6bcea4202 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/ICanvasBrushElement.cs @@ -0,0 +1,40 @@ +// 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.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush +{ + /// + /// Interface for a Brush Element + /// + internal interface ICanvasBrushElement + { + /// + /// Gets the Brush data defining the Brush Element + /// + string Data { get; } + + /// + /// Gets the number of non-whitespace characters in + /// the Brush Data + /// + int ValidationCount { get; } + + /// + /// Initializes the Brush Element with the given Capture + /// + /// Capture object + void Initialize(Capture capture); + + /// + /// Creates the ICanvasBrush from the parsed data + /// + /// ICanvasResourceCreator object + /// ICanvasBrush + ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator); + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientBrushElement.cs new file mode 100644 index 00000000000..412343ef16b --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientBrushElement.cs @@ -0,0 +1,206 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers; +using Windows.UI; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush +{ + /// + /// Represents a CanvasLinearGradientBrush with GradientStops + /// + internal sealed class LinearGradientBrushElement : AbstractCanvasBrushElement + { + private Vector2 _startPoint; + private Vector2 _endPoint; + private CanvasAlphaMode _alphaMode; + private CanvasBufferPrecision _bufferPrecision; + private CanvasEdgeBehavior _edgeBehavior; + private CanvasColorSpace _preInterpolationColorSpace; + private CanvasColorSpace _postInterpolationColorSpace; + private List _gradientStops; + + /// + /// Initializes a new instance of the class. + /// + /// Capture object + public LinearGradientBrushElement(Capture capture) + { + // Set the default values + _startPoint = Vector2.Zero; + _endPoint = Vector2.Zero; + _opacity = 1f; + _alphaMode = (CanvasAlphaMode)0; + _bufferPrecision = (CanvasBufferPrecision)0; + _edgeBehavior = (CanvasEdgeBehavior)0; + + // Default ColorSpace is sRGB + _preInterpolationColorSpace = CanvasColorSpace.Srgb; + _postInterpolationColorSpace = CanvasColorSpace.Srgb; + _gradientStops = new List(); + + // Initialize + Initialize(capture); + } + + /// + /// Creates the CanvasLinearGradientBrush from the parsed data + /// + /// ICanvasResourceCreator object + /// An instance of + public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator) + { + var brush = new CanvasLinearGradientBrush( + resourceCreator, + _gradientStops.ToArray(), + _edgeBehavior, + _alphaMode, + _preInterpolationColorSpace, + _postInterpolationColorSpace, + _bufferPrecision) + { + StartPoint = _startPoint, + EndPoint = _endPoint, + Opacity = _opacity + }; + + return brush; + } + + /// + /// Gets the Regex for extracting Brush Element Attributes + /// + /// Regex + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(BrushType.LinearGradient); + } + + /// + /// Gets the Brush Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // Start Point + float.TryParse(match.Groups["StartX"].Value, out var startX); + float.TryParse(match.Groups["StartY"].Value, out var startY); + _startPoint = new Vector2(startX, startY); + + // End Point + float.TryParse(match.Groups["EndX"].Value, out var endX); + float.TryParse(match.Groups["EndY"].Value, out var endY); + _endPoint = new Vector2(endX, endY); + + // Opacity (optional) + var group = match.Groups["Opacity"]; + if (group.Success) + { + float.TryParse(group.Value, out _opacity); + } + + // Alpha Mode (optional) + group = match.Groups["AlphaMode"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _alphaMode); + } + + // Buffer Precision (optional) + group = match.Groups["BufferPrecision"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _bufferPrecision); + } + + // Edge Behavior (optional) + group = match.Groups["EdgeBehavior"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _edgeBehavior); + } + + // Pre Interpolation ColorSpace (optional) + group = match.Groups["PreColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _preInterpolationColorSpace); + } + + // Post Interpolation ColorSpace (optional) + group = match.Groups["PostColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _postInterpolationColorSpace); + } + + // GradientStops + group = match.Groups["GradientStops"]; + if (group.Success) + { + _gradientStops.Clear(); + foreach (Capture capture in group.Captures) + { + var gradientMatch = RegexFactory.GradientStopRegex.Match(capture.Value); + if (!gradientMatch.Success) + { + continue; + } + + float position; + Color color; + + // Main Attributes + var main = gradientMatch.Groups["Main"]; + if (main.Success) + { + var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Main).Match(main.Value); + float.TryParse(mainMatch.Groups["Position"].Value, out position); + color = ColorParser.Parse(mainMatch); + + _gradientStops.Add(new CanvasGradientStop() + { + Color = color, + Position = position + }); + } + + // Additional Attributes + var additional = gradientMatch.Groups["Additional"]; + if (!additional.Success) + { + continue; + } + + foreach (Capture addCapture in additional.Captures) + { + var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Additional).Match(addCapture.Value); + float.TryParse(addMatch.Groups["Position"].Value, out position); + color = ColorParser.Parse(addMatch); + + _gradientStops.Add(new CanvasGradientStop() + { + Color = color, + Position = position + }); + } + } + + // Sort the stops based on their position + if (_gradientStops.Any()) + { + _gradientStops = _gradientStops.OrderBy(g => g.Position).ToList(); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientHdrBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientHdrBrushElement.cs new file mode 100644 index 00000000000..1a348439fb2 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientHdrBrushElement.cs @@ -0,0 +1,212 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush +{ + /// + /// Represents a CanvasLinearGradientBrush with GradientStopHdrs + /// + internal sealed class LinearGradientHdrBrushElement : AbstractCanvasBrushElement + { + private Vector2 _startPoint; + private Vector2 _endPoint; + private CanvasAlphaMode _alphaMode; + private CanvasBufferPrecision _bufferPrecision; + private CanvasEdgeBehavior _edgeBehavior; + private CanvasColorSpace _preInterpolationColorSpace; + private CanvasColorSpace _postInterpolationColorSpace; + private List _gradientStopHdrs; + + /// + /// Initializes a new instance of the class. + /// + /// Capture object + public LinearGradientHdrBrushElement(Capture capture) + { + // Set the default values + _startPoint = Vector2.Zero; + _endPoint = Vector2.Zero; + _opacity = 1f; + _alphaMode = (CanvasAlphaMode)0; + _bufferPrecision = (CanvasBufferPrecision)0; + _edgeBehavior = (CanvasEdgeBehavior)0; + + // Default ColorSpace is sRGB + _preInterpolationColorSpace = CanvasColorSpace.Srgb; + _postInterpolationColorSpace = CanvasColorSpace.Srgb; + _gradientStopHdrs = new List(); + + // Initialize + Initialize(capture); + } + + /// + /// Creates the CanvasLinearGradientBrush from the parsed data + /// + /// ICanvasResourceCreator object + /// Instance of + public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator) + { + var brush = CanvasLinearGradientBrush.CreateHdr( + resourceCreator, + _gradientStopHdrs.ToArray(), + _edgeBehavior, + _alphaMode, + _preInterpolationColorSpace, + _postInterpolationColorSpace, + _bufferPrecision); + + brush.StartPoint = _startPoint; + brush.EndPoint = _endPoint; + brush.Opacity = _opacity; + + return brush; + } + + /// + /// Gets the Regex for extracting Brush Element Attributes + /// + /// Regex + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(BrushType.LinearGradientHdr); + } + + /// + /// Gets the Brush Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // Start Point + float.TryParse(match.Groups["StartX"].Value, out var startX); + float.TryParse(match.Groups["StartY"].Value, out var startY); + _startPoint = new Vector2(startX, startY); + + // End Point + float.TryParse(match.Groups["EndX"].Value, out var endX); + float.TryParse(match.Groups["EndY"].Value, out var endY); + _endPoint = new Vector2(endX, endY); + + // Opacity (optional) + var group = match.Groups["Opacity"]; + if (group.Success) + { + float.TryParse(group.Value, out _opacity); + } + + // Alpha Mode (optional) + group = match.Groups["AlphaMode"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _alphaMode); + } + + // Buffer Precision (optional) + group = match.Groups["BufferPrecision"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _bufferPrecision); + } + + // Edge Behavior (optional) + group = match.Groups["EdgeBehavior"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _edgeBehavior); + } + + // Pre Interpolation ColorSpace (optional) + group = match.Groups["PreColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _preInterpolationColorSpace); + } + + // Post Interpolation ColorSpace (optional) + group = match.Groups["PostColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _postInterpolationColorSpace); + } + + // GradientStopHdrs + group = match.Groups["GradientStops"]; + if (group.Success) + { + _gradientStopHdrs.Clear(); + foreach (Capture capture in group.Captures) + { + var gradientMatch = RegexFactory.GradientStopHdrRegex.Match(capture.Value); + if (!gradientMatch.Success) + { + continue; + } + + // Main Attributes + var main = gradientMatch.Groups["Main"]; + if (main.Success) + { + var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.MainHdr) + .Match(main.Value); + float.TryParse(mainMatch.Groups["Position"].Value, out float position); + float.TryParse(mainMatch.Groups["X"].Value, out float x); + float.TryParse(mainMatch.Groups["Y"].Value, out float y); + float.TryParse(mainMatch.Groups["Z"].Value, out float z); + float.TryParse(mainMatch.Groups["W"].Value, out float w); + + var color = new Vector4(x, y, z, w); + + _gradientStopHdrs.Add(new CanvasGradientStopHdr() + { + Color = color, + Position = position + }); + } + + // Additional Attributes + var additional = gradientMatch.Groups["Additional"]; + if (!additional.Success) + { + continue; + } + + foreach (Capture addCapture in additional.Captures) + { + var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.AdditionalHdr) + .Match(addCapture.Value); + float.TryParse(addMatch.Groups["Position"].Value, out float position); + float.TryParse(addMatch.Groups["X"].Value, out float x); + float.TryParse(addMatch.Groups["Y"].Value, out float y); + float.TryParse(addMatch.Groups["Z"].Value, out float z); + float.TryParse(addMatch.Groups["W"].Value, out float w); + + var color = new Vector4(x, y, z, w); + + _gradientStopHdrs.Add(new CanvasGradientStopHdr() + { + Color = color, + Position = position + }); + } + } + + // Sort the stops based on their position + if (_gradientStopHdrs.Any()) + { + _gradientStopHdrs = _gradientStopHdrs.OrderBy(g => g.Position).ToList(); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientBrushElement.cs new file mode 100644 index 00000000000..9e47a089886 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientBrushElement.cs @@ -0,0 +1,230 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers; +using Windows.UI; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush +{ + /// + /// Represents a CanvasRadialGradientBrush with GradientStops + /// + internal sealed class RadialGradientBrushElement : AbstractCanvasBrushElement + { + private float _radiusX; + private float _radiusY; + private Vector2 _center; + private Vector2 _originOffset; + private CanvasAlphaMode _alphaMode; + private CanvasBufferPrecision _bufferPrecision; + private CanvasEdgeBehavior _edgeBehavior; + private CanvasColorSpace _preInterpolationColorSpace; + private CanvasColorSpace _postInterpolationColorSpace; + private List _gradientStops; + + /// + /// Initializes a new instance of the class. + /// + /// Capture object + public RadialGradientBrushElement(Capture capture) + { + // Set the default values + _radiusX = 0f; + _radiusY = 0f; + _center = Vector2.Zero; + _originOffset = Vector2.Zero; + _opacity = 1f; + _alphaMode = (CanvasAlphaMode)0; + _bufferPrecision = (CanvasBufferPrecision)0; + _edgeBehavior = (CanvasEdgeBehavior)0; + + // Default ColorSpace is sRGB + _preInterpolationColorSpace = CanvasColorSpace.Srgb; + _postInterpolationColorSpace = CanvasColorSpace.Srgb; + _gradientStops = new List(); + + // Initialize + Initialize(capture); + } + + /// + /// Creates the CanvasLinearGradientBrush from the parsed data + /// + /// ICanvasResourceCreator object + /// Instance of + public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator) + { + var brush = new CanvasRadialGradientBrush( + resourceCreator, + _gradientStops.ToArray(), + _edgeBehavior, + _alphaMode, + _preInterpolationColorSpace, + _postInterpolationColorSpace, + _bufferPrecision) + { + RadiusX = _radiusX, + RadiusY = _radiusY, + Center = _center, + OriginOffset = _originOffset, + Opacity = _opacity + }; + + return brush; + } + + /// + /// Gets the Regex for extracting Brush Element Attributes + /// + /// Regex + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(BrushType.RadialGradient); + } + + /// + /// Gets the Brush Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // RadiusX + float.TryParse(match.Groups["RadiusX"].Value, out _radiusX); + + // Sanitize by taking the absolute value + _radiusX = Math.Abs(_radiusX); + + // RadiusY + float.TryParse(match.Groups["RadiusY"].Value, out _radiusY); + + // Sanitize by taking the absolute value + _radiusY = Math.Abs(_radiusY); + + // CenterX + float.TryParse(match.Groups["CenterX"].Value, out var centerX); + + // CenterY + float.TryParse(match.Groups["CenterY"].Value, out var centerY); + _center = new Vector2(centerX, centerY); + + // Opacity (optional) + var group = match.Groups["Opacity"]; + if (group.Success) + { + float.TryParse(group.Value, out _opacity); + } + + // Origin Offset (optional) + group = match.Groups["OriginOffset"]; + if (group.Success) + { + float.TryParse(match.Groups["OffsetX"].Value, out var offsetX); + float.TryParse(match.Groups["OffsetY"].Value, out var offsetY); + _originOffset = new Vector2(offsetX, offsetY); + } + + // Alpha Mode (optional) + group = match.Groups["AlphaMode"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _alphaMode); + } + + // Buffer Precision (optional) + group = match.Groups["BufferPrecision"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _bufferPrecision); + } + + // Edge Behavior (optional) + group = match.Groups["EdgeBehavior"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _edgeBehavior); + } + + // Pre Interpolation ColorSpace (optional) + group = match.Groups["PreColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _preInterpolationColorSpace); + } + + // Post Interpolation ColorSpace (optional) + group = match.Groups["PostColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _postInterpolationColorSpace); + } + + // Gradient Stops + group = match.Groups["GradientStops"]; + if (group.Success) + { + _gradientStops.Clear(); + foreach (Capture capture in group.Captures) + { + var gradientMatch = RegexFactory.GradientStopRegex.Match(capture.Value); + if (!gradientMatch.Success) + { + continue; + } + + float position; + Color color; + + // Main Attributes + var main = gradientMatch.Groups["Main"]; + if (main.Success) + { + var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Main).Match(main.Value); + float.TryParse(mainMatch.Groups["Position"].Value, out position); + color = ColorParser.Parse(mainMatch); + + _gradientStops.Add(new CanvasGradientStop() + { + Color = color, + Position = position + }); + } + + // Additional Attributes + var additional = gradientMatch.Groups["Additional"]; + if (!additional.Success) + { + continue; + } + + foreach (Capture addCapture in additional.Captures) + { + var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Additional).Match(addCapture.Value); + float.TryParse(addMatch.Groups["Position"].Value, out position); + color = ColorParser.Parse(addMatch); + + _gradientStops.Add(new CanvasGradientStop() + { + Color = color, + Position = position + }); + } + } + + // Sort the stops based on their position + if (_gradientStops.Any()) + { + _gradientStops = _gradientStops.OrderBy(g => g.Position).ToList(); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientHdrBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientHdrBrushElement.cs new file mode 100644 index 00000000000..e1143d38eaf --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientHdrBrushElement.cs @@ -0,0 +1,237 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush +{ + /// + /// Represents a CanvasRadialGradientBrush with GradientStopHdrs + /// + internal sealed class RadialGradientHdrBrushElement : AbstractCanvasBrushElement + { + private float _radiusX; + private float _radiusY; + private Vector2 _center; + private Vector2 _originOffset; + private CanvasAlphaMode _alphaMode; + private CanvasBufferPrecision _bufferPrecision; + private CanvasEdgeBehavior _edgeBehavior; + private CanvasColorSpace _preInterpolationColorSpace; + private CanvasColorSpace _postInterpolationColorSpace; + private List _gradientStopHdrs; + + /// + /// Initializes a new instance of the class. + /// + /// Capture object + public RadialGradientHdrBrushElement(Capture capture) + { + // Set the default values + _radiusX = 0f; + _radiusY = 0f; + _center = Vector2.Zero; + _originOffset = Vector2.Zero; + _opacity = 1f; + _alphaMode = (CanvasAlphaMode)0; + _bufferPrecision = (CanvasBufferPrecision)0; + _edgeBehavior = (CanvasEdgeBehavior)0; + + // Default ColorSpace is sRGB + _preInterpolationColorSpace = CanvasColorSpace.Srgb; + _postInterpolationColorSpace = CanvasColorSpace.Srgb; + _gradientStopHdrs = new List(); + + // Initialize + Initialize(capture); + } + + /// + /// Creates the CanvasLinearGradientBrush from the parsed data + /// + /// ICanvasResourceCreator object + /// Instance of + public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator) + { + var brush = CanvasRadialGradientBrush.CreateHdr( + resourceCreator, + _gradientStopHdrs.ToArray(), + _edgeBehavior, + _alphaMode, + _preInterpolationColorSpace, + _postInterpolationColorSpace, + _bufferPrecision); + + brush.RadiusX = _radiusX; + brush.RadiusY = _radiusY; + brush.Center = _center; + brush.OriginOffset = _originOffset; + brush.Opacity = _opacity; + + return brush; + } + + /// + /// Gets the Regex for extracting Brush Element Attributes + /// + /// Regex + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(BrushType.RadialGradientHdr); + } + + /// + /// Gets the Brush Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // RadiusX + float.TryParse(match.Groups["RadiusX"].Value, out _radiusX); + + // Sanitize by taking the absolute value + _radiusX = Math.Abs(_radiusX); + + // RadiusY + float.TryParse(match.Groups["RadiusY"].Value, out _radiusY); + + // Sanitize by taking the absolute value + _radiusY = Math.Abs(_radiusY); + + // CenterX + float.TryParse(match.Groups["CenterX"].Value, out var centerX); + + // CenterY + float.TryParse(match.Groups["CenterY"].Value, out var centerY); + _center = new Vector2(centerX, centerY); + + // Opacity (optional) + var group = match.Groups["Opacity"]; + if (group.Success) + { + float.TryParse(group.Value, out _opacity); + } + + // Origin Offset (optional) + group = match.Groups["OriginOffset"]; + if (group.Success) + { + float.TryParse(match.Groups["OffsetX"].Value, out var offsetX); + float.TryParse(match.Groups["OffsetY"].Value, out var offsetY); + _originOffset = new Vector2(offsetX, offsetY); + } + + // Alpha Mode (optional) + group = match.Groups["AlphaMode"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _alphaMode); + } + + // Buffer Precision (optional) + group = match.Groups["BufferPrecision"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _bufferPrecision); + } + + // Edge Behavior (optional) + group = match.Groups["EdgeBehavior"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _edgeBehavior); + } + + // Pre Interpolation ColorSpace (optional) + group = match.Groups["PreColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _preInterpolationColorSpace); + } + + // Post Interpolation ColorSpace (optional) + group = match.Groups["PostColorSpace"]; + if (group.Success) + { + Enum.TryParse(group.Value, out _postInterpolationColorSpace); + } + + // GradientStopHdrs + group = match.Groups["GradientStops"]; + if (group.Success) + { + _gradientStopHdrs.Clear(); + foreach (Capture capture in group.Captures) + { + var gradientMatch = RegexFactory.GradientStopHdrRegex.Match(capture.Value); + if (!gradientMatch.Success) + { + continue; + } + + float position; + float x = 0, y = 0, z = 0, w = 0; + var main = gradientMatch.Groups["Main"]; + if (main.Success) + { + var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.MainHdr) + .Match(main.Value); + + float.TryParse(mainMatch.Groups["Position"].Value, out position); + float.TryParse(mainMatch.Groups["X"].Value, out x); + float.TryParse(mainMatch.Groups["Y"].Value, out y); + float.TryParse(mainMatch.Groups["Z"].Value, out z); + float.TryParse(mainMatch.Groups["W"].Value, out w); + + var color = new Vector4(x, y, z, w); + + _gradientStopHdrs.Add(new CanvasGradientStopHdr() + { + Color = color, + Position = position + }); + } + + var additional = gradientMatch.Groups["Additional"]; + if (!additional.Success) + { + continue; + } + + foreach (Capture addCapture in additional.Captures) + { + var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.AdditionalHdr) + .Match(addCapture.Value); + float.TryParse(addMatch.Groups["Position"].Value, out position); + float.TryParse(addMatch.Groups["X"].Value, out x); + float.TryParse(addMatch.Groups["Y"].Value, out y); + float.TryParse(addMatch.Groups["Z"].Value, out z); + float.TryParse(addMatch.Groups["W"].Value, out w); + + var color = new Vector4(x, y, z, w); + + _gradientStopHdrs.Add(new CanvasGradientStopHdr() + { + Color = color, + Position = position + }); + } + } + + // Sort the stops based on their position + if (_gradientStopHdrs.Any()) + { + _gradientStopHdrs = _gradientStopHdrs.OrderBy(g => g.Position).ToList(); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/SolidColorBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/SolidColorBrushElement.cs new file mode 100644 index 00000000000..7314835f7b9 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/SolidColorBrushElement.cs @@ -0,0 +1,73 @@ +// 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.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers; +using Windows.UI; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush +{ + /// + /// Represents a CanvasSolidColorBrush + /// + internal sealed class SolidColorBrushElement : AbstractCanvasBrushElement + { + private Color _color; + + /// + /// Initializes a new instance of the class. + /// + /// Capture object + public SolidColorBrushElement(Capture capture) + { + // Set the default values + _color = Colors.Transparent; + _opacity = 1f; + + Initialize(capture); + } + + /// + /// Creates the ICanvasBrush from the parsed data + /// + /// ICanvasResourceCreator object + /// Instance of + public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator) + { + return new CanvasSolidColorBrush(resourceCreator, _color) + { + Opacity = _opacity + }; + } + + /// + /// Gets the Regex for extracting Brush Element Attributes + /// + /// Regex + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(BrushType.SolidColor); + } + + /// + /// Gets the Brush Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // Parse the Color + _color = ColorParser.Parse(match); + + // Opacity (optional) + var group = match.Groups["Opacity"]; + if (group.Success) + { + float.TryParse(group.Value, out _opacity); + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/AbstractPathElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/AbstractPathElement.cs new file mode 100644 index 00000000000..f6fb00c4f00 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/AbstractPathElement.cs @@ -0,0 +1,107 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Abstract base class for all Path Elements + /// + internal abstract class AbstractPathElement : ICanvasPathElement + { + /// + /// Gets or sets index of the Path Element in the Path Data + /// + public int Index { get; protected set; } = -1; + + /// + /// Gets or sets path data defining the Path Element + /// + public string Data { get; protected set; } = string.Empty; + + /// + /// Gets or sets number of non-whitespace characters in + /// the Path Element Data + /// + public int ValidationCount { get; protected set; } = 0; + + /// + /// Gets or sets a value indicating whether the path element contains + /// absolute or relative coordinates. + /// + public bool IsRelative { get; protected set; } = false; + + /// + /// Initializes the Path Element with the given Match + /// + /// Match object + /// Index within the match + public virtual void Initialize(Match match, int index) + { + var main = match.Groups["Main"]; + Index = index; + Data = main.Value; + var command = match.Groups["Command"].Value[0]; + IsRelative = char.IsLower(command); + + // Get the Path Element attributes + GetAttributes(match); + + // Get the number of non-whitespace characters in the data + ValidationCount = RegexFactory.ValidationRegex.Replace(main.Value, string.Empty).Length; + } + + /// + /// Initializes the Path Element with the given Capture + /// + /// Capture object + /// Index of the Path Element in the Path data. + /// Indicates whether the Path Element coordinates are + /// absolute or relative + public virtual void InitializeAdditional(Capture capture, int index, bool isRelative) + { + Index = index; + Data = capture.Value; + IsRelative = isRelative; + + var match = GetAttributesRegex().Match(Data); + if (match.Captures.Count != 1) + { + return; + } + + // Get the Path Element attributes + GetAttributes(match); + + // Get the number of non-whitespace characters in the data + ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public abstract Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement); + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected abstract Regex GetAttributesRegex(); + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected abstract void GetAttributes(Match match); + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ArcElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ArcElement.cs new file mode 100644 index 00000000000..9026d44f924 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ArcElement.cs @@ -0,0 +1,101 @@ +// 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; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Arc Element in a Path Geometry + /// + internal class ArcElement : AbstractPathElement + { + private float _radiusX; + private float _radiusY; + private float _angle; + private CanvasArcSize _arcSize; + private CanvasSweepDirection _sweepDirection; + private float _x; + private float _y; + + /// + /// Initializes a new instance of the class. + /// + public ArcElement() + { + _radiusX = _radiusY = _angle = 0; + _arcSize = CanvasArcSize.Small; + _sweepDirection = CanvasSweepDirection.Clockwise; + _sweepDirection = 0; + _x = _y = 0; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var point = new Vector2(_x, _y); + if (IsRelative) + { + point += currentPoint; + } + + // Execute command + pathBuilder.AddArc(point, _radiusX, _radiusY, _angle, _sweepDirection, _arcSize); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.Arc); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["RadiusX"].Value, out _radiusX); + + // Sanitize by taking only the absolute value + _radiusX = Math.Abs(_radiusX); + float.TryParse(match.Groups["RadiusY"].Value, out _radiusY); + + // Sanitize by taking only the absolute value + _radiusY = Math.Abs(_radiusY); + + // Angle is provided in degrees + float.TryParse(match.Groups["Angle"].Value, out _angle); + + // Convert angle to radians as CanvasPathBuilder.AddArc() method + // requires the angle to be in radians + _angle *= Scalar.DegreesToRadians; + Enum.TryParse(match.Groups["IsLargeArc"].Value, out _arcSize); + Enum.TryParse(match.Groups["SweepDirection"].Value, out _sweepDirection); + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasEllipseFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasEllipseFigure.cs new file mode 100644 index 00000000000..7f07c806e40 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasEllipseFigure.cs @@ -0,0 +1,83 @@ +// 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; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Ellipse Figure in a Path Geometry + /// + internal class CanvasEllipseFigure : AbstractPathElement + { + private float _radiusX; + private float _radiusY; + private float _x; + private float _y; + + /// + /// Initializes a new instance of the class. + /// + public CanvasEllipseFigure() + { + _radiusX = _radiusY = _x = _y = 0; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the EllipseFigure + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the EllipseFigure + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var center = new Vector2(_x, _y); + if (IsRelative) + { + center += currentPoint; + } + + // Execute command + pathBuilder.AddEllipseFigure(center.X, center.Y, _radiusX, _radiusY); + + // No need to update the lastElement or currentPoint here as we are creating + // a separate closed figure here. So current point will not change. + return currentPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathFigureType.EllipseFigure); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["RadiusX"].Value, out _radiusX); + + // Sanitize by taking the absolute value + _radiusX = Math.Abs(_radiusX); + float.TryParse(match.Groups["RadiusY"].Value, out _radiusY); + + // Sanitize by taking the absolute value + _radiusY = Math.Abs(_radiusY); + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPathFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPathFigure.cs new file mode 100644 index 00000000000..63df1371edc --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPathFigure.cs @@ -0,0 +1,136 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class which contains a collection of ICanvasPathElements + /// which can be used to create CanvasGeometry. + /// + internal class CanvasPathFigure : AbstractPathElement + { + // Collection of Path Elements + private List _elements; + + public CanvasPathFigure() + { + _elements = new List(); + ValidationCount = 0; + } + + /// + /// Initializes the Path Element with the given Match + /// + /// Match object + /// Index of the path element in the Path data. + public override void Initialize(Match match, int index) + { + Index = index; + var main = match.Groups["Main"]; + Data = main.Value; + + var elements = new List(); + foreach (PathElementType type in Enum.GetValues(typeof(PathElementType))) + { + foreach (Capture elementCapture in match.Groups[type.ToString()].Captures) + { + var elementRootIndex = elementCapture.Index; + var regex = RegexFactory.GetRegex(type); + var elementMatch = regex.Match(elementCapture.Value); + var isRelative = false; + + // Process the 'Main' Group which contains the Path Command and + // corresponding attributes + if (elementMatch.Groups["Main"].Captures.Count == 1) + { + var figure = PathElementFactory.CreatePathElement(type, elementMatch, elementRootIndex); + elements.Add(figure); + isRelative = figure.IsRelative; + } + + // Process the 'Additional' Group which contains just the attributes + elements.AddRange(from Capture capture in elementMatch.Groups["Additional"].Captures + select PathElementFactory.CreateAdditionalPathElement(type, capture, elementRootIndex + capture.Index, isRelative)); + } + } + + // Sort the path elements based on their index value + _elements.AddRange(elements.OrderBy(e => e.Index)); + if (_elements.Count <= 0) + { + return; + } + + // Check if the last path element in the figure is an ClosePathElement + // which would indicate that the path needs to be closed. Otherwise, + // add a default ClosePathElement at the end to indicate that the path + // is not closed. + var lastElement = _elements.ElementAt(_elements.Count - 1); + if ((lastElement as ClosePathElement) == null) + { + _elements.Add(PathElementFactory.CreateDefaultPathElement(PathElementType.ClosePath)); + } + + // Validation Count will be the cumulative sum of the validation count + // of child elements of the PathFigure + ValidationCount = _elements.Sum(x => x.ValidationCount); + } + + /// + /// Initializes the Path Element with the given Capture + /// + /// Capture object + /// Index of the Path Element in the Path data. + /// Indicates whether the Path Element coordinates are + /// absolute or relative + public override void InitializeAdditional(Capture capture, int index, bool isRelative) + { + // Do nothing as this scenario is not valid for CanvasPathFigure + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + foreach (var pathElement in _elements) + { + currentPoint = pathElement.CreatePath(pathBuilder, currentPoint, ref lastElement); + } + + return currentPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return null; + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // Do nothing + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPolygonFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPolygonFigure.cs new file mode 100644 index 00000000000..806d1060385 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPolygonFigure.cs @@ -0,0 +1,85 @@ +// 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; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Polygon Figure in a Path Geometry + /// + internal class CanvasPolygonFigure : AbstractPathElement + { + private int _numSides; + private float _radius; + private float _x; + private float _y; + + public CanvasPolygonFigure() + { + _numSides = 0; + _radius = _x = _y = 0; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the PolygonFigure + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the PolygonFigure + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var center = new Vector2(_x, _y); + if (IsRelative) + { + center += currentPoint; + } + + // Execute command + pathBuilder.AddPolygonFigure(_numSides, center.X, center.Y, _radius); + + // No need to update the lastElement or currentPoint here as we are creating + // a separate closed figure here. So current point will not change. + return currentPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathFigureType.PolygonFigure); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // Sides + int.TryParse(match.Groups["Sides"].Value, out _numSides); + + // Sanitize by taking the absolute value + _numSides = Math.Abs(_numSides); + + // Radius + float.TryParse(match.Groups["Radius"].Value, out _radius); + + // Sanitize by taking the absolute value + _radius = Math.Abs(_radius); + + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRectangleFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRectangleFigure.cs new file mode 100644 index 00000000000..c691fee2602 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRectangleFigure.cs @@ -0,0 +1,90 @@ +// 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; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Rectangle Figure in a Path Geometry + /// + internal sealed class CanvasRectangleFigure : AbstractPathElement + { + private float _x; + private float _y; + private float _width; + private float _height; + + /// + /// Initializes a new instance of the class. + /// + public CanvasRectangleFigure() + { + _x = _y = _width = _height = 0f; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the PolygonFigure + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the PolygonFigure + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var topLeft = new Vector2(_x, _y); + if (IsRelative) + { + topLeft += currentPoint; + } + + // Execute command + pathBuilder.AddRectangleFigure(topLeft.X, topLeft.Y, _width, _height); + + // No need to update the lastElement or currentPoint here as we are creating + // a separate closed figure here.So current point will not change. + return currentPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathFigureType.RectangleFigure); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // X + float.TryParse(match.Groups["X"].Value, out _x); + + // Y + float.TryParse(match.Groups["Y"].Value, out _y); + + // Width + float.TryParse(match.Groups["Width"].Value, out _width); + + // Sanitize by taking the absolute value + _width = Math.Abs(_width); + + // Height + float.TryParse(match.Groups["Height"].Value, out _height); + + // Sanitize by taking the absolute value + _height = Math.Abs(_height); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRoundRectangleFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRoundRectangleFigure.cs new file mode 100644 index 00000000000..f9fa6027f8a --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRoundRectangleFigure.cs @@ -0,0 +1,104 @@ +// 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; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the RoundRectangle Figure in a Path Geometry + /// + internal sealed class CanvasRoundRectangleFigure : AbstractPathElement + { + private float _x; + private float _y; + private float _width; + private float _height; + private float _radiusX; + private float _radiusY; + + /// + /// Initializes a new instance of the class. + /// + public CanvasRoundRectangleFigure() + { + _x = _y = _width = _height = _radiusX = _radiusY = 0f; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the PolygonFigure + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the PolygonFigure + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var topLeft = new Vector2(_x, _y); + if (IsRelative) + { + topLeft += currentPoint; + } + + // Execute command + pathBuilder.AddRoundedRectangleFigure(topLeft.X, topLeft.Y, _width, _height, _radiusX, _radiusY); + + // No need to update the lastElement or currentPoint here as we are creating + // a separate closed figure here.So current point will not change. + return currentPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathFigureType.RoundedRectangleFigure); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // X + float.TryParse(match.Groups["X"].Value, out _x); + + // Y + float.TryParse(match.Groups["Y"].Value, out _y); + + // Width + float.TryParse(match.Groups["Width"].Value, out _width); + + // Sanitize by taking the absolute value + _width = Math.Abs(_width); + + // Height + float.TryParse(match.Groups["Height"].Value, out _height); + + // Sanitize by taking the absolute value + _height = Math.Abs(_height); + + // RadiusX + float.TryParse(match.Groups["RadiusX"].Value, out _radiusX); + + // Sanitize by taking the absolute value + _radiusX = Math.Abs(_radiusX); + + // RadiusY + float.TryParse(match.Groups["RadiusY"].Value, out _radiusY); + + // Sanitize by taking the absolute value + _radiusY = Math.Abs(_radiusY); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ClosePathElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ClosePathElement.cs new file mode 100644 index 00000000000..61ffb6255bb --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ClosePathElement.cs @@ -0,0 +1,101 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the ClosePath command in a Path Geometry + /// + internal class ClosePathElement : AbstractPathElement + { + private bool _isFigureClosed; + + /// + /// Initializes a new instance of the class. + /// + public ClosePathElement() + { + _isFigureClosed = false; + } + + /// + /// Initializes the Path Element with the given Match + /// + /// Match object + /// Index of the Path Element in the Path data. + public override void Initialize(Match match, int index) + { + var main = match.Groups["Main"]; + Index = index; + Data = main.Value; + var command = match.Groups["Command"]; + + // If the Command is captured, it means that 'Z' is provided and hence + // the figure must be closed + if (command.Captures.Count == 1) + { + _isFigureClosed = true; + } + + // Get the number of non-whitespace characters in the data + ValidationCount = RegexFactory.ValidationRegex.Replace(main.Value, string.Empty).Length; + } + + /// + /// Initializes the Path Element with the given Capture + /// + /// Capture object + /// Index of the Path Element in the Path data. + /// Indicates whether the Path Element coordinates are + /// absolute or relative + public override void InitializeAdditional(Capture capture, int index, bool isRelative) + { + // Do nothing as this scenario is not valid for this Path Element + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Execute command + pathBuilder.EndFigure(_isFigureClosed ? CanvasFigureLoop.Closed : CanvasFigureLoop.Open); + + // Set Last Element + lastElement = this; + + // Return current point + return currentPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + // Attributes are not present + return null; + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + // Do Nothing + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CubicBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CubicBezierElement.cs new file mode 100644 index 00000000000..282d7740b22 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CubicBezierElement.cs @@ -0,0 +1,105 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Cubic Bezier Element in a Path Geometry + /// + internal class CubicBezierElement : AbstractPathElement + { + private float _x1; + private float _y1; + private float _x2; + private float _y2; + private float _x; + private float _y; + + private Vector2 _absoluteControlPoint2; + + /// + /// Initializes a new instance of the class. + /// + public CubicBezierElement() + { + _x1 = _y1 = 0; + _x2 = _y2 = 0; + _x = _y = 0; + _absoluteControlPoint2 = Vector2.Zero; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var controlPoint1 = new Vector2(_x1, _y1); + var controlPoint2 = new Vector2(_x2, _y2); + var point = new Vector2(_x, _y); + + if (IsRelative) + { + controlPoint1 += currentPoint; + controlPoint2 += currentPoint; + point += currentPoint; + } + + // Save the second absolute control point so that it can be used by the following + // SmoothCubicBezierElement (if any) + _absoluteControlPoint2 = controlPoint2; + + // Execute command + pathBuilder.AddCubicBezier(controlPoint1, controlPoint2, point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Gets the Second Control Point of this Cubic Bezier + /// + /// Vector2 + public Vector2 GetControlPoint() + { + return _absoluteControlPoint2; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.CubicBezier); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["X1"].Value, out _x1); + float.TryParse(match.Groups["Y1"].Value, out _y1); + float.TryParse(match.Groups["X2"].Value, out _x2); + float.TryParse(match.Groups["Y2"].Value, out _y2); + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/FillRuleElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/FillRuleElement.cs new file mode 100644 index 00000000000..897219f3401 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/FillRuleElement.cs @@ -0,0 +1,78 @@ +// 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; +using System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Fill Rule Element in a Path Geometry + /// + internal class FillRuleElement : AbstractPathElement + { + private CanvasFilledRegionDetermination _fillValue; + + /// + /// Initializes a new instance of the class. + /// + public FillRuleElement() + { + _fillValue = CanvasFilledRegionDetermination.Alternate; + } + + /// + /// Initializes the Path Element with the given Capture + /// + /// Capture object + /// Index of the Path Element in the Path data. + /// Indicates whether the Path Element coordinates are + /// absolute or relative + public override void InitializeAdditional(Capture capture, int index, bool isRelative) + { + // Do nothing as this scenario is not valid for this Path Element + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Execute command + pathBuilder.SetFilledRegionDetermination(_fillValue); + + // Set Last Element + lastElement = this; + + // Return current point + return currentPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + // Not applicable for this Path Element + return null; + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + Enum.TryParse(match.Groups["FillValue"].Value, out _fillValue); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/HorizontalLineElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/HorizontalLineElement.cs new file mode 100644 index 00000000000..4b98fcc0d59 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/HorizontalLineElement.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 System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Horizontal Line Element in a Path Geometry + /// + internal class HorizontalLineElement : AbstractPathElement + { + private float _x; + + /// + /// Initializes a new instance of the class. + /// + public HorizontalLineElement() + { + _x = 0; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var point = IsRelative ? + new Vector2(currentPoint.X + _x, currentPoint.Y) : new Vector2(_x, currentPoint.Y); + + // Execute command + pathBuilder.AddLine(point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.HorizontalLine); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["X"].Value, out _x); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ICanvasPathElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ICanvasPathElement.cs new file mode 100644 index 00000000000..fe57f2e4a4a --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ICanvasPathElement.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; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Interface for a Path Element which serves + /// as a building block for CanvasPathGeometry + /// + internal interface ICanvasPathElement + { + /// + /// Gets index of the Path Element in the Path Data + /// + int Index { get; } + + /// + /// Gets path data defining the Path Element + /// + string Data { get; } + + /// + /// Gets number of non-whitespace characters in + /// the Path Element Data + /// + int ValidationCount { get; } + + /// + /// Gets a value indicating whether the path element contains + /// absolute or relative coordinates. + /// + bool IsRelative { get; } + + /// + /// Initializes the Path Element with the given Match + /// + /// Match object + /// Index of the Path Element in the Path data. + void Initialize(Match match, int index); + + /// + /// Initializes the Path Element with the given Capture + /// + /// Capture object + /// Index of the Path Element in the Path data. + /// Indicates whether the Path Element coordinates are + /// absolute or relative + void InitializeAdditional(Capture capture, int index, bool isRelative); + + /// + /// Adds the Path Element to the PathBuilder. + /// + /// CanvasPathBuilder object + /// The current point on the path before the path element is added + /// The previous PathElement in the Path. + /// The current point on the path after the path element is added + Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement); + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/LineElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/LineElement.cs new file mode 100644 index 00000000000..186b63f18cd --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/LineElement.cs @@ -0,0 +1,74 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Line Element in a Path Geometry + /// + internal class LineElement : AbstractPathElement + { + private float _x; + private float _y; + + /// + /// Initializes a new instance of the class. + /// + public LineElement() + { + _x = _y = 0; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var point = new Vector2(_x, _y); + if (IsRelative) + { + point += currentPoint; + } + + // Execute command + pathBuilder.AddLine(point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.Line); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/MoveToElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/MoveToElement.cs new file mode 100644 index 00000000000..29d85724ecd --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/MoveToElement.cs @@ -0,0 +1,87 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the MoveTo Command in a Path Geometry + /// + internal class MoveToElement : AbstractPathElement + { + private float _x; + private float _y; + + /// + /// Initializes a new instance of the class. + /// + public MoveToElement() + { + _x = _y = 0; + } + + /// + /// Initializes the Path Element with the given Capture + /// + /// Capture object + /// Index of the Path Element in the Path data. + /// Indicates whether the Path Element coordinates are + /// absolute or relative + public override void InitializeAdditional(Capture capture, int index, bool isRelative) + { + // Do nothing as this scenario is not valid for MoveTo Command + // Additional coordinates specified with MoveTo will be converted to Line Commands + } + + /// + /// Adds the Path Element to the PathBuilder. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var point = new Vector2(_x, _y); + if (IsRelative) + { + point += currentPoint; + } + + // Execute command + pathBuilder.BeginFigure(point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.MoveTo); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/QuadraticBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/QuadraticBezierElement.cs new file mode 100644 index 00000000000..d697610ae75 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/QuadraticBezierElement.cs @@ -0,0 +1,97 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Quadratic Bezier Element in a Path Geometry + /// + internal class QuadraticBezierElement : AbstractPathElement + { + private float _x1; + private float _y1; + private float _x; + private float _y; + private Vector2 _absoluteControlPoint; + + /// + /// Initializes a new instance of the class. + /// + public QuadraticBezierElement() + { + _x1 = _y1 = 0; + _x = _y = 0; + _absoluteControlPoint = Vector2.Zero; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var controlPoint = new Vector2(_x1, _y1); + var point = new Vector2(_x, _y); + + if (IsRelative) + { + controlPoint += currentPoint; + point += currentPoint; + } + + // Save the absolute control point so that it can be used by the following + // SmoothQuadraticBezierElement (if any) + _absoluteControlPoint = controlPoint; + + // Execute command + pathBuilder.AddQuadraticBezier(controlPoint, point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Gets the Control Point of this Quadratic Bezier + /// + /// Vector2 + public Vector2 GetControlPoint() + { + return _absoluteControlPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.QuadraticBezier); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["X1"].Value, out _x1); + float.TryParse(match.Groups["Y1"].Value, out _y1); + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothCubicBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothCubicBezierElement.cs new file mode 100644 index 00000000000..6c6c820966d --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothCubicBezierElement.cs @@ -0,0 +1,117 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Smooth Cubic Bezier Element in a Path Geometry + /// + internal class SmoothCubicBezierElement : AbstractPathElement + { + private float _x2; + private float _y2; + private float _x; + private float _y; + private Vector2 _absoluteControlPoint2; + + /// + /// Initializes a new instance of the class. + /// + public SmoothCubicBezierElement() + { + _x2 = _y2 = 0; + _x = _y = 0; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + // Check if the last element was a Cubic Bezier + Vector2 controlPoint1; + if (lastElement is CubicBezierElement cubicBezier) + { + // Reflect the second control point of the cubic bezier over the current point. The + // resulting point will be the first control point of this Bezier. + controlPoint1 = Utils.Reflect(cubicBezier.GetControlPoint(), currentPoint); + } + + // Or if the last element was s Smooth Cubic Bezier + else + { + // If the last element was a Smooth Cubic Bezier then reflect its second control point + // over the current point. The resulting point will be the first control point of this + // Bezier. Otherwise, if the last element was not a Smooth Cubic Bezier then the + // currentPoint will be the first control point of this Bezier + controlPoint1 = lastElement is SmoothCubicBezierElement smoothCubicBezier + ? Utils.Reflect(smoothCubicBezier.GetControlPoint(), currentPoint) + : currentPoint; + } + + var controlPoint2 = new Vector2(_x2, _y2); + var point = new Vector2(_x, _y); + + if (IsRelative) + { + controlPoint2 += currentPoint; + point += currentPoint; + } + + // Save the second absolute control point so that it can be used by the following + // SmoothCubicBezierElement (if any) + _absoluteControlPoint2 = controlPoint2; + + // Execute command + pathBuilder.AddCubicBezier(controlPoint1, controlPoint2, point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Gets the Second Control Point of this Cubic Bezier + /// + /// Vector2 + public Vector2 GetControlPoint() + { + return _absoluteControlPoint2; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.SmoothCubicBezier); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["X2"].Value, out _x2); + float.TryParse(match.Groups["Y2"].Value, out _y2); + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothQuadraticBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothQuadraticBezierElement.cs new file mode 100644 index 00000000000..444a3262e14 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothQuadraticBezierElement.cs @@ -0,0 +1,106 @@ +// 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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Smooth Quadratic Bezier Element in a Path Geometry + /// + internal class SmoothQuadraticBezierElement : AbstractPathElement + { + private float _x; + private float _y; + private Vector2 _absoluteControlPoint; + + /// + /// Initializes a new instance of the class. + /// + public SmoothQuadraticBezierElement() + { + _x = _y = 0; + _absoluteControlPoint = Vector2.Zero; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + // Check if the last element was a Quadratic Bezier + if (lastElement is QuadraticBezierElement quadBezier) + { + // Reflect the control point of the Quadratic Bezier over the current point. The + // resulting point will be the control point of this Bezier. + _absoluteControlPoint = Utils.Reflect(quadBezier.GetControlPoint(), currentPoint); + } + + // Or if the last element was s Smooth Quadratic Bezier + else + { + // If the last element was a Smooth Quadratic Bezier then reflect its control point + // over the current point. The resulting point will be the control point of this + // Bezier. Otherwise, if the last element was not a Smooth Quadratic Bezier, + // then the currentPoint will be the control point of this Bezier. + _absoluteControlPoint = lastElement is SmoothQuadraticBezierElement smoothQuadBezier + ? Utils.Reflect(smoothQuadBezier.GetControlPoint(), currentPoint) + : currentPoint; + } + + var point = new Vector2(_x, _y); + + if (IsRelative) + { + point += currentPoint; + } + + // Execute command + pathBuilder.AddQuadraticBezier(_absoluteControlPoint, point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Gets the Control Point of this Quadratic Bezier + /// + /// Vector2 + public Vector2 GetControlPoint() + { + return _absoluteControlPoint; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.SmoothQuadraticBezier); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["X"].Value, out _x); + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/VerticalLineElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/VerticalLineElement.cs new file mode 100644 index 00000000000..2b06607c4c7 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/VerticalLineElement.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 System.Numerics; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path +{ + /// + /// Class representing the Vertical Line Element in a Path Geometry + /// + internal class VerticalLineElement : AbstractPathElement + { + private float _y; + + /// + /// Initializes a new instance of the class. + /// + public VerticalLineElement() + { + _y = 0; + } + + /// + /// Adds the Path Element to the Path. + /// + /// CanvasPathBuilder object + /// The last active location in the Path before adding + /// the Path Element + /// The previous PathElement in the Path. + /// The latest location in the Path after adding the Path Element + public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement) + { + // Calculate coordinates + var point = IsRelative ? + new Vector2(currentPoint.X, currentPoint.Y + _y) : new Vector2(currentPoint.X, _y); + + // Execute command + pathBuilder.AddLine(point); + + // Set Last Element + lastElement = this; + + // Return current point + return point; + } + + /// + /// Get the Regex for extracting Path Element Attributes + /// + /// Instance of + protected override Regex GetAttributesRegex() + { + return RegexFactory.GetAttributesRegex(PathElementType.VerticalLine); + } + + /// + /// Gets the Path Element Attributes from the Match + /// + /// Match object + protected override void GetAttributes(Match match) + { + float.TryParse(match.Groups["Y"].Value, out _y); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/AbstractCanvasStrokeElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/AbstractCanvasStrokeElement.cs new file mode 100644 index 00000000000..ce2e8f4f2c6 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/AbstractCanvasStrokeElement.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.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke +{ + /// + /// Abstract base class for Stroke Element. + /// + internal abstract class AbstractCanvasStrokeElement : ICanvasStrokeElement + { + /// + /// Gets or sets the Stroke data defining the Brush Element + /// + public string Data { get; protected set; } + + /// + /// Gets or sets the number of non-whitespace characters in the Stroke Data. + /// + public int ValidationCount { get; protected set; } + + /// + /// Initializes the Stroke Element with the given Capture. + /// + /// Match object + public virtual void Initialize(Match match) + { + Data = match.Value; + + if (!match.Success) + { + return; + } + + GetAttributes(match); + + // Update the validation count + Validate(); + } + + /// + /// Gets the number of non-whitespace characters in the data. + /// + protected virtual void Validate() + { + ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length; + } + + /// + /// Creates the ICanvasStroke from the parsed data. + /// + /// ICanvasStroke + public abstract ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator); + + /// + /// Gets the Stroke Element Attributes from the Match. + /// + /// Match object + protected abstract void GetAttributes(Match match); + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeElement.cs new file mode 100644 index 00000000000..419dc1f4269 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeElement.cs @@ -0,0 +1,105 @@ +// 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; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke +{ + /// + /// Represents a Stroke Element. + /// + internal sealed class CanvasStrokeElement : AbstractCanvasStrokeElement + { + private float _width; + private ICanvasBrushElement _brush; + private ICanvasStrokeStyleElement _style; + private int _widthValidationCount; + + /// + /// Initializes a new instance of the class. + /// + /// Match object + public CanvasStrokeElement(Match match) + { + _width = 1f; + _brush = null; + _style = null; + _widthValidationCount = 0; + + Initialize(match); + } + + /// + /// Creates the ICanvasStroke from the parsed data. + /// + /// ICanvasStroke + public override ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator) + { + return new CanvasStroke(_brush.CreateBrush(resourceCreator), _width, _style.Style); + } + + /// + /// Gets the Stroke Element Attributes from the Match. + /// + /// Match object + protected override void GetAttributes(Match match) + { + // Stroke Width + var group = match.Groups["StrokeWidth"]; + float.TryParse(group.Value, out _width); + + // Sanitize by taking the absolute value + _width = Math.Abs(_width); + + _widthValidationCount = RegexFactory.ValidationRegex.Replace(group.Value, string.Empty).Length; + + // Stroke Brush + group = match.Groups["CanvasBrush"]; + if (group.Success) + { + _brush = CanvasBrushParser.Parse(group.Value); + } + + // If the ICanvasBrushElement was not created, then the ICanvasStroke cannot be created + if (_brush == null) + { + static void Throw(string value) => throw new ArgumentException($"Unable to create a valid ICanvasBrush for the ICanvasStroke with the following Brush data - '{value}'."); + + Throw(group.Value); + } + + // Stroke Style + _style = CanvasStrokeStyleParser.Parse(match); + } + + /// + /// Gets the number of non-whitespace characters in the data. + /// + protected override void Validate() + { + // Add 2 to the Validation Count to include the stroke command 'ST' + ValidationCount += 2; + + // StrokeWidth Validation Count + ValidationCount += _widthValidationCount; + + // Stroke Brush Validation Count + if (_brush != null) + { + ValidationCount += _brush.ValidationCount; + } + + // Stroke Style Validation Count + if (_style != null) + { + ValidationCount += _style.ValidationCount; + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeStyleElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeStyleElement.cs new file mode 100644 index 00000000000..79e2d396d15 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeStyleElement.cs @@ -0,0 +1,182 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke +{ + /// + /// Represents a CanvasStrokeStyle Element. + /// + internal sealed class CanvasStrokeStyleElement : ICanvasStrokeStyleElement + { + /// + /// Gets the Stroke data defining the Brush Element + /// + public string Data { get; private set; } + + /// + /// Gets the number of non-whitespace characters in + /// the Stroke Data + /// + public int ValidationCount { get; private set; } + + /// + /// Gets the CanvasStrokeStyle obtained by parsing + /// the style data. + /// + public CanvasStrokeStyle Style { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The matching data + public CanvasStrokeStyleElement(Match match) + { + Data = match.Groups["CanvasStrokeStyle"].Value; + + Style = new CanvasStrokeStyle(); + + Initialize(match); + + // Get the number of non-whitespace characters in the data + ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length; + } + + /// + /// Initializes the Stroke Element with the given Capture. + /// + /// Match object + public void Initialize(Match match) + { + var group = match.Groups["CanvasStrokeStyle"]; + if (group.Success) + { + // DashStyle + group = match.Groups["DashStyle"]; + if (group.Success) + { + Enum.TryParse(group.Value, out CanvasDashStyle dashStyle); + Style.DashStyle = dashStyle; + } + + // LineJoin + group = match.Groups["LineJoin"]; + if (group.Success) + { + Enum.TryParse(group.Value, out CanvasLineJoin lineJoin); + Style.LineJoin = lineJoin; + } + + // MiterLimit + group = match.Groups["MiterLimit"]; + if (group.Success) + { + float.TryParse(group.Value, out var miterLimit); + + // Sanitize by taking the absolute value + Style.MiterLimit = Math.Abs(miterLimit); + } + + // DashOffset + group = match.Groups["DashOffset"]; + if (group.Success) + { + float.TryParse(group.Value, out var dashOffset); + Style.DashOffset = dashOffset; + } + + // StartCap + group = match.Groups["StartCap"]; + if (group.Success) + { + Enum.TryParse(group.Value, out CanvasCapStyle capStyle); + Style.StartCap = capStyle; + } + + // EndCap + group = match.Groups["EndCap"]; + if (group.Success) + { + Enum.TryParse(group.Value, out CanvasCapStyle capStyle); + Style.EndCap = capStyle; + } + + // DashCap + group = match.Groups["DashCap"]; + if (group.Success) + { + Enum.TryParse(group.Value, out CanvasCapStyle capStyle); + Style.DashCap = capStyle; + } + + // TransformBehavior + group = match.Groups["TransformBehavior"]; + if (group.Success) + { + Enum.TryParse(group.Value, out CanvasStrokeTransformBehavior transformBehavior); + Style.TransformBehavior = transformBehavior; + } + + // CustomDashStyle + group = match.Groups["CustomDashStyle"]; + if (group.Success) + { + List dashes = new List(); + group = match.Groups["Main"]; + if (group.Success) + { + if (float.TryParse(match.Groups["DashSize"].Value, out var dashSize)) + { + // Sanitize by taking the absolute value + dashes.Add(Math.Abs(dashSize)); + } + + if (float.TryParse(match.Groups["SpaceSize"].Value, out var spaceSize)) + { + // Sanitize by taking the absolute value + dashes.Add(Math.Abs(spaceSize)); + } + } + + group = match.Groups["Additional"]; + if (group.Success) + { + foreach (Capture capture in group.Captures) + { + var dashMatch = RegexFactory.CustomDashAttributeRegex.Match(capture.Value); + if (!dashMatch.Success) + { + continue; + } + + if (float.TryParse(dashMatch.Groups["DashSize"].Value, out var dashSize)) + { + // Sanitize by taking the absolute value + dashes.Add(Math.Abs(dashSize)); + } + + if (float.TryParse(dashMatch.Groups["SpaceSize"].Value, out var spaceSize)) + { + // Sanitize by taking the absolute value + dashes.Add(Math.Abs(spaceSize)); + } + } + } + + // Any valid dashes? + if (dashes.Any()) + { + Style.CustomDashStyle = dashes.ToArray(); + } + } + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeElement.cs new file mode 100644 index 00000000000..4cec2b20473 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeElement.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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke +{ + /// + /// Interface for Stroke Element + /// + internal interface ICanvasStrokeElement + { + /// + /// Gets the Stroke data defining the Brush Element. + /// + string Data { get; } + + /// + /// Gets the number of non-whitespace characters in the Stroke Data. + /// + int ValidationCount { get; } + + /// + /// Initializes the Stroke Element with the given Capture. + /// + /// Match object + void Initialize(Match match); + + /// + /// Creates the ICanvasStroke from the parsed data. + /// + /// ICanvasStroke + ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator); + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeStyleElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeStyleElement.cs new file mode 100644 index 00000000000..5a7fee6e268 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeStyleElement.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 System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke +{ + /// + /// Interface for the CanvasStrokeStyle Element + /// + internal interface ICanvasStrokeStyleElement + { + /// + /// Gets the Stroke data defining the Brush Element. + /// + string Data { get; } + + /// + /// Gets the number of non-whitespace characters in the Stroke Data. + /// + int ValidationCount { get; } + + /// + /// Gets the CanvasStrokeStyle obtained by parsing the style data. + /// + CanvasStrokeStyle Style { get; } + + /// + /// Initializes the Stroke Element with the given Capture. + /// + /// Match object + void Initialize(Match match); + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/ICanvasStroke.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/ICanvasStroke.cs new file mode 100644 index 00000000000..e4209e85dcf --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/ICanvasStroke.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 System.Numerics; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Graphics.Canvas.Geometry; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Interface to represent the Stroke which can be used to render an outline on a . + /// + public interface ICanvasStroke + { + /// + /// Gets or sets the brush with which the will be rendered. + /// + ICanvasBrush Brush { get; set; } + + /// + /// Gets or sets the width of the . + /// + float Width { get; set; } + + /// + /// Gets or sets the Style of the . + /// + CanvasStrokeStyle Style { get; set; } + + /// + /// Gets or sets transform matrix of the brush. + /// + Matrix3x2 Transform { get; set; } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasBrushParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasBrushParser.cs new file mode 100644 index 00000000000..6f63b4cb518 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasBrushParser.cs @@ -0,0 +1,122 @@ +// 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; +using System.Runtime.CompilerServices; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Brushes; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers +{ + /// + /// Parser for ICanvasBrush. + /// + internal static class CanvasBrushParser + { + /// + /// Parses the Brush data string and converts it into ICanvasBrushElement. + /// + /// Brush data + /// ICanvasBrushElement + internal static ICanvasBrushElement Parse(string brushData) + { + var matches = RegexFactory.CanvasBrushRegex.Matches(brushData); + + // If no match is found or no captures in the match, then it means that the brush data is invalid. + if (matches.Count == 0) + { + ThrowForZeroCount(); + } + + // If the match contains more than one captures, it means that there are multiple brushes present in the brush data. + // There should be only one brush defined in the brush data. + if (matches.Count > 1) + { + ThrowForNotOneCount(); + } + + // There should be only one match + var match = matches[0]; + AbstractCanvasBrushElement brushElement = null; + if (match.Groups["SolidColorBrush"].Success) + { + brushElement = new SolidColorBrushElement(match.Groups["SolidColorBrush"].Captures[0]); + } + else if (match.Groups["LinearGradient"].Success) + { + brushElement = new LinearGradientBrushElement(match.Groups["LinearGradient"].Captures[0]); + } + else if (match.Groups["LinearGradientHdr"].Success) + { + brushElement = new LinearGradientHdrBrushElement(match.Groups["LinearGradientHdr"].Captures[0]); + } + else if (match.Groups["RadialGradient"].Success) + { + brushElement = new RadialGradientBrushElement(match.Groups["RadialGradient"].Captures[0]); + } + else if (match.Groups["RadialGradientHdr"].Success) + { + brushElement = new RadialGradientHdrBrushElement(match.Groups["RadialGradientHdr"].Captures[0]); + } + + if (brushElement == null) + { + return null; + } + + // Perform validation to check if there are any invalid characters in the brush data that were not captured + var preValidationCount = RegexFactory.ValidationRegex.Replace(brushData, string.Empty).Length; + var postValidationCount = brushElement.ValidationCount; + + // If there are invalid characters, extract them and add them to the ArgumentException message + if (preValidationCount != postValidationCount) + { + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowForInvalidCharacters(AbstractCanvasBrushElement brushElement, string brushData) + { + var parseIndex = 0; + if (!string.IsNullOrWhiteSpace(brushElement.Data)) + { + parseIndex = brushData.IndexOf(brushElement.Data, parseIndex, StringComparison.Ordinal); + } + + var errorString = brushData.Substring(parseIndex); + if (errorString.Length > 30) + { + errorString = $"{errorString.Substring(0, 30)}..."; + } + + throw new ArgumentException($"BRUSH_ERR003:Brush data contains invalid characters!\nIndex: {parseIndex}\n{errorString}"); + } + + ThrowForInvalidCharacters(brushElement, brushData); + } + + return brushElement; + + static void ThrowForZeroCount() => throw new ArgumentException("BRUSH_ERR001:Invalid Brush data! No matching brush data found!"); + static void ThrowForNotOneCount() => throw new ArgumentException("BRUSH_ERR002:Multiple Brushes defined in Brush Data! " + + "There should be only one Brush definition within the Brush Data. " + + "You can either remove Brush definitions or split the Brush Data " + + "into multiple Brush Data and call the CanvasPathGeometry.CreateBrush() method on each of them."); + } + + /// + /// Parses the Brush data string and converts it into ICanvasBrush. + /// + /// ICanvasResourceCreator + /// Brush data string + /// ICanvasBrush + internal static ICanvasBrush Parse(ICanvasResourceCreator resourceCreator, string brushData) + { + // Parse the brush data to get the ICanvasBrushElement + var brushElement = Parse(brushData); + + // Create ICanvasBrush from the brushElement + return brushElement.CreateBrush(resourceCreator); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasGeometryParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasGeometryParser.cs new file mode 100644 index 00000000000..df11070fedd --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasGeometryParser.cs @@ -0,0 +1,137 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers +{ + /// + /// Parser for CanvasGeometry. + /// + internal static class CanvasGeometryParser + { + /// + /// Parses the Path data in string format and converts it to . + /// + /// ICanvasResourceCreator + /// Path data + /// + internal static CanvasGeometry Parse(ICanvasResourceCreator resourceCreator, string pathData) + { + var pathFigures = new List(); + + var matches = RegexFactory.CanvasGeometryRegex.Matches(pathData); + + // If no match is found or no captures in the match, then it means // that the path data is invalid. + if (matches.Count == 0) + { + ThrowForZeroCount(); + } + + // If the match contains more than one captures, it means that there are multiple FillRuleElements present in the path data. + // There can be only one FillRuleElement in the path data (at the beginning). + if (matches.Count > 1) + { + ThrowForNotOneCount(); + } + + var figures = new List(); + + foreach (PathFigureType type in Enum.GetValues(typeof(PathFigureType))) + { + foreach (Capture figureCapture in matches[0].Groups[type.ToString()].Captures) + { + var figureRootIndex = figureCapture.Index; + var regex = RegexFactory.GetRegex(type); + var figureMatch = regex.Match(figureCapture.Value); + if (!figureMatch.Success) + { + continue; + } + + // Process the 'Main' Group which contains the Path Command and + // corresponding attributes + var figure = PathElementFactory.CreatePathFigure(type, figureMatch, figureRootIndex); + figures.Add(figure); + + // Process the 'Additional' Group which contains just the attributes + figures.AddRange(from Capture capture in figureMatch.Groups["Additional"].Captures + select PathElementFactory.CreateAdditionalPathFigure(type, capture, figureRootIndex + capture.Index, figure.IsRelative)); + } + } + + // Sort the figures by their indices + pathFigures.AddRange(figures.OrderBy(f => f.Index)); + if (pathFigures.Count > 0) + { + // Check if the first element in the _figures list is a FillRuleElement + // which would indicate the fill rule to be followed while creating the + // path. If it is not present, then insert a default FillRuleElement at + // the beginning. + if ((pathFigures.ElementAt(0) as FillRuleElement) == null) + { + pathFigures.Insert(0, PathElementFactory.CreateDefaultPathElement(PathFigureType.FillRule)); + } + } + else + { + return null; + } + + // Perform validation to check if there are any invalid characters in the path data that were not captured + var preValidationCount = RegexFactory.ValidationRegex.Replace(pathData, string.Empty).Length; + var postValidationCount = pathFigures.Sum(x => x.ValidationCount); + + // If there are invalid characters, extract them and add them to the ArgumentException message + if (preValidationCount != postValidationCount) + { + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowForInvalidCharacters(List pathFigures, string pathData) + { + var parseIndex = 0; + foreach (var figure in pathFigures) + { + parseIndex = pathData.IndexOf(figure.Data, parseIndex, StringComparison.Ordinal) + figure.Data.Length; + } + + var errorString = pathData.Substring(parseIndex); + if (errorString.Length > 30) + { + errorString = $"{errorString.Substring(0, 30)}..."; + } + + throw new ArgumentException($"PATH_ERR003:Path data contains invalid characters!\nIndex: {parseIndex}\n{errorString}"); + } + + ThrowForInvalidCharacters(pathFigures, pathData); + } + + ICanvasPathElement lastElement = null; + var currentPoint = Vector2.Zero; + + using var pathBuilder = new CanvasPathBuilder(resourceCreator); + foreach (var pathFigure in pathFigures) + { + currentPoint = pathFigure.CreatePath(pathBuilder, currentPoint, ref lastElement); + } + + return CanvasGeometry.CreatePath(pathBuilder); + + static void ThrowForZeroCount() => throw new ArgumentException("PATH_ERR000:Invalid Path data! No matching path data found!"); + static void ThrowForNotOneCount() => throw new ArgumentException("PATH_ERR001:Multiple FillRule elements present in Path Data!\n" + + "There should be only one FillRule within the Path Data. " + + "You can either remove additional FillRule elements or split the Path Data " + + "into multiple Path Data and call the CanvasPathGeometry.CreateGeometry() method on each of them."); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeParser.cs new file mode 100644 index 00000000000..9f91969fb27 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeParser.cs @@ -0,0 +1,98 @@ +// 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; +using System.Runtime.CompilerServices; +using Microsoft.Graphics.Canvas; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers +{ + /// + /// Parser for CanvasStroke + /// + internal static class CanvasStrokeParser + { + /// + /// Parses the Stroke Data string and converts it into ICanvasStrokeElement. + /// + /// Stroke Data string + /// ICanvasStrokeElement + internal static ICanvasStrokeElement Parse(string strokeData) + { + var matches = RegexFactory.CanvasStrokeRegex.Matches(strokeData); + + // If no match is found or no captures in the match, then it means + // that the stroke data is invalid. + if (matches.Count == 0) + { + ThrowForZeroCount(); + } + + // If the match contains more than one captures, it means that there + // are multiple CanvasStrokes present in the stroke data. There should + // be only one CanvasStroke defined in the stroke data. + if (matches.Count > 1) + { + ThrowForNotOneCount(); + } + + // There should be only one match + var match = matches[0]; + var strokeElement = new CanvasStrokeElement(match); + + // Perform validation to check if there are any invalid characters in the stroke data that were not captured + var preValidationCount = RegexFactory.ValidationRegex.Replace(strokeData, string.Empty).Length; + var postValidationCount = strokeElement.ValidationCount; + + // If there are invalid characters, extract them and add them to the ArgumentException message + if (preValidationCount != postValidationCount) + { + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowForInvalidCharacters(CanvasStrokeElement strokeElement, string strokeData) + { + var parseIndex = 0; + if (!string.IsNullOrWhiteSpace(strokeElement.Data)) + { + parseIndex = strokeData.IndexOf(strokeElement.Data, parseIndex, StringComparison.Ordinal); + } + + var errorString = strokeData.Substring(parseIndex); + if (errorString.Length > 30) + { + errorString = $"{errorString.Substring(0, 30)}..."; + } + + throw new ArgumentException($"STROKE_ERR003:Stroke data contains invalid characters!\nIndex: {parseIndex}\n{errorString}"); + } + + ThrowForInvalidCharacters(strokeElement, strokeData); + } + + return strokeElement; + + static void ThrowForZeroCount() => throw new ArgumentException("STROKE_ERR001:Invalid Stroke data! No matching CanvasStroke found!"); + static void ThrowForNotOneCount() => throw new ArgumentException("STROKE_ERR002:Multiple CanvasStrokes defined in Stroke Data! " + + "There should be only one CanvasStroke definition within the Stroke Data. " + + "You can either remove CanvasStroke definitions or split the Stroke Data " + + "into multiple Stroke Data and call the CanvasPathGeometry.CreateStroke() method on each of them."); + } + + /// + /// Parses the Stroke Data string and converts it into CanvasStroke. + /// + /// ICanvasResourceCreator + /// Stroke Data string + /// ICanvasStroke + internal static ICanvasStroke Parse(ICanvasResourceCreator resourceCreator, string strokeData) + { + // Parse the stroke data to create the ICanvasStrokeElement + var strokeElement = Parse(strokeData); + + // Create the CanvasStroke from the strokeElement + return strokeElement.CreateStroke(resourceCreator); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeStyleParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeStyleParser.cs new file mode 100644 index 00000000000..c0418ee5619 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeStyleParser.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; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.Graphics.Canvas.Geometry; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers +{ + /// + /// Parser for the CanvasStrokeStyle + /// + internal static class CanvasStrokeStyleParser + { + /// + /// Parses the given style data and converts it to CanvasStrokeStyle. + /// + /// Style data + /// CanvasStrokeStyle + internal static CanvasStrokeStyle Parse(string styleData) + { + var matches = RegexFactory.CanvasStrokeStyleRegex.Matches(styleData); + + // If no match is found or no captures in the match, then it means that the style data is invalid. + if (matches.Count == 0) + { + ThrowForZeroCount(); + } + + // If the match contains more than one captures, it means that there + // are multiple CanvasStrokeStyles present in the CanvasStrokeStyle data. There should + // be only one CanvasStrokeStyle defined in the CanvasStrokeStyle data. + if (matches.Count > 1) + { + ThrowForNotOneCount(); + } + + // There should be only one match + var match = matches[0]; + var styleElement = new CanvasStrokeStyleElement(match); + + // Perform validation to check if there are any invalid characters in the brush data that were not captured + var preValidationCount = RegexFactory.ValidationRegex.Replace(styleData, string.Empty).Length; + var postValidationCount = styleElement.ValidationCount; + + // If there are invalid characters, extract them and add them to the ArgumentException message + if (preValidationCount != postValidationCount) + { + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowForInvalidCharacters(CanvasStrokeStyleElement styleElement, string styleData) + { + var parseIndex = 0; + if (!string.IsNullOrWhiteSpace(styleElement.Data)) + { + parseIndex = styleData.IndexOf(styleElement.Data, parseIndex, StringComparison.Ordinal); + } + + var errorString = styleData.Substring(parseIndex); + if (errorString.Length > 30) + { + errorString = $"{errorString.Substring(0, 30)}..."; + } + + throw new ArgumentException($"STYLE_ERR003:Style data contains invalid characters!\nIndex: {parseIndex}\n{errorString}"); + } + + ThrowForInvalidCharacters(styleElement, styleData); + } + + return styleElement.Style; + + static void ThrowForZeroCount() => throw new ArgumentException("STYLE_ERR001:Invalid CanvasStrokeStyle data! No matching CanvasStrokeStyle found!"); + static void ThrowForNotOneCount() => throw new ArgumentException("STYLE_ERR002:Multiple CanvasStrokeStyles defined in CanvasStrokeStyle Data! " + + "There should be only one CanvasStrokeStyle definition within the CanvasStrokeStyle Data. " + + "You can either remove CanvasStrokeStyle definitions or split the CanvasStrokeStyle Data " + + "into multiple CanvasStrokeStyle Data and call the CanvasPathGeometry.CreateStrokeStyle() method on each of them."); + } + + /// + /// Parses and constructs a ICanvasStrokeStyleElement from the specified Match object. + /// + /// Match object + /// ICanvasStrokeStyleElement + internal static ICanvasStrokeStyleElement Parse(Match match) + { + return new CanvasStrokeStyleElement(match); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/ColorParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/ColorParser.cs new file mode 100644 index 00000000000..3bc1955d473 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/ColorParser.cs @@ -0,0 +1,146 @@ +// 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; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core; +using Windows.UI; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers +{ + /// + /// Parser for Color + /// + internal static class ColorParser + { + /// + /// Converts the color string in Hexadecimal or HDR color format to the corresponding Color object. + /// The hexadecimal color string should be in #RRGGBB or #AARRGGBB format. + /// The '#' character is optional. + /// The HDR color string should be in R G B A format. + /// (R, G, B & A should have value in the range between 0 and 1, inclusive) + /// + /// Color string in Hexadecimal or HDR format + /// Color + internal static Color Parse(string colorString) + { + var match = RegexFactory.ColorRegex.Match(colorString); + + if (!match.Success) + { + ThrowArgumentException(); + } + + // Perform validation to check if there are any invalid characters in the colorString that were not captured + var preValidationCount = RegexFactory.ValidationRegex.Replace(colorString, string.Empty).Length; + var postValidationCount = RegexFactory.ValidationRegex.Replace(match.Value, string.Empty).Length; + + // If there are invalid characters, extract them and add them to the ArgumentException message + if (preValidationCount != postValidationCount) + { + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowForInvalidCharacters(Match match, string colorString) + { + var parseIndex = 0; + if (!string.IsNullOrWhiteSpace(match.Value)) + { + parseIndex = colorString.IndexOf(match.Value, parseIndex, StringComparison.Ordinal); + } + + var errorString = colorString.Substring(parseIndex); + if (errorString.Length > 30) + { + errorString = $"{errorString.Substring(0, 30)}..."; + } + + throw new ArgumentException($"COLOR_ERR003:Color data contains invalid characters!\nIndex: {parseIndex}\n{errorString}"); + } + + ThrowForInvalidCharacters(match, colorString); + } + + return Parse(match); + + static void ThrowArgumentException() => throw new ArgumentException("COLOR_ERR001:Invalid value provided in Color Data! No matching color found in the Color Data."); + } + + /// + /// Converts a Vector4 High Dynamic Range Color to Color. Negative components of the Vector4 will be sanitized by taking the absolute value of the component. + /// The HDR Color components should have value in the range between 0 and 1, inclusive. If they are more than 1, they will be clamped at 1. + /// + /// High Dynamic Range Color + /// Color + internal static Color Parse(Vector4 hdrColor) + { + // Vector4's X, Y, Z, W components match to + // Color's R, G, B, A components respectively + return Parse(hdrColor.X, hdrColor.Y, hdrColor.Z, hdrColor.W); + } + + /// + /// Converts the given HDR color values to Color. + /// + /// Red Component + /// Green Component + /// Blue Component + /// Alpha Component + /// Instance of Color. + internal static Color Parse(float x, float y, float z, float w) + { + Vector4 v4 = new Vector4(x, y, z, w); + v4 = Vector4.Min(Vector4.Abs(v4) * 255f, new Vector4(255f)); + + var r = (byte)v4.X; + var g = (byte)v4.Y; + var b = (byte)v4.Z; + var a = (byte)v4.W; + + return Color.FromArgb(a, r, g, b); + } + + /// + /// Parses and constructs a Color object from the specified Match object. + /// + /// Match object + /// Color + internal static Color Parse(Match match) + { + if (match.Groups["RgbColor"].Success) + { + // Alpha component + byte alpha = 255; + var alphaStr = match.Groups["Alpha"].Value; + if (!string.IsNullOrWhiteSpace(alphaStr)) + { + alpha = (byte)Convert.ToInt32(alphaStr, 16); + } + + // Red component + var red = (byte)Convert.ToInt32(match.Groups["Red"].Value, 16); + + // Green component + var green = (byte)Convert.ToInt32(match.Groups["Green"].Value, 16); + + // Blue component + var blue = (byte)Convert.ToInt32(match.Groups["Blue"].Value, 16); + + return Color.FromArgb(alpha, red, green, blue); + } + + if (match.Groups["HdrColor"].Success) + { + float.TryParse(match.Groups["X"].Value, out var x); + float.TryParse(match.Groups["Y"].Value, out var y); + float.TryParse(match.Groups["Z"].Value, out var z); + float.TryParse(match.Groups["W"].Value, out var w); + + return Parse(x, y, z, w); + } + + return Colors.Transparent; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Scalar.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Scalar.cs new file mode 100644 index 00000000000..e93b1b62af7 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Scalar.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. + +using System; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Class containing some constants + /// represented as Floating point numbers. + /// + internal static class Scalar + { + // Pi related floating point constants + + /// + /// (float)Math.PI radians ( or 180 degrees). + /// + internal const float Pi = (float)Math.PI; + + /// + /// Two times (float)Math.PI radians ( or 360 degrees). + /// + internal const float TwoPi = 2f * Pi; + + /// + /// Half of (float)Math.PI radians ( or 90 degrees). + /// + internal const float PiByTwo = Pi / 2f; + + /// + /// One third of (float)Math.PI radians ( or 60 degrees). + /// + internal const float PiByThree = Pi / 3f; + + /// + /// One fourth of (float)Math.PI radians ( or 45 degrees). + /// + internal const float PiByFour = Pi / 4f; + + /// + /// One sixth of (float)Math.PI radians ( or 30 degrees). + /// + internal const float PiBySix = Pi / 6f; + + /// + /// Three times half of (float)Math.PI radians ( or 270 degrees). + /// + internal const float ThreePiByTwo = 3f * Pi / 2f; + + // Conversion constants + + /// + /// 1 degree in radians. + /// + internal const float DegreesToRadians = Pi / 180f; + + /// + /// 1 radian in degrees. + /// + internal const float RadiansToDegrees = 180f / Pi; + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Utils.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Utils.cs new file mode 100644 index 00000000000..9a893a799a6 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Utils.cs @@ -0,0 +1,817 @@ +// 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; +using System.Numerics; +using System.Runtime.InteropServices; +using Windows.Foundation; +using Windows.UI; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry +{ + /// + /// Class containing collection of useful methods for various types + /// + public static class Utils + { + // Constant values + + // Smallest double value such that 1.0 + DoubleEpsilon != 1.0 + internal const double DoubleEpsilon = 2.2250738585072014E-308; + + // Smallest float value such that 1.0f + FloatMin != 1.0f + internal const float FloatMin = 1.175494351E-38F; + + /// + /// Returns whether or not two doubles are "close". + /// + /// The first double to compare. + /// The second double to compare. + /// + /// bool - the result of the AreClose comparision. + /// + public static bool IsCloseTo(this double value1, double value2) + { + // In case they are Infinities or NaN (then epsilon check does not work) + if ((double.IsInfinity(value1) && + double.IsInfinity(value2)) || + (double.IsNaN(value1) && double.IsNaN(value2))) + { + return true; + } + + // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DoubleEpsilon + var eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon; + var delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + /// + /// Returns whether or not the first double is less than the second double. + /// + /// The first double to compare. + /// The second double to compare. + /// + /// bool - the result of the LessThan comparision. + /// + public static bool IsLessThan(this double value1, double value2) + { + return (value1 < value2) && !value1.IsCloseTo(value2); + } + + /// + /// Returns whether or not the first double is greater than the second double. + /// + /// The first double to compare. + /// The second double to compare. + /// + /// bool - the result of the GreaterThan comparision. + /// + public static bool IsGreaterThan(this double value1, double value2) + { + return (value1 > value2) && !value1.IsCloseTo(value2); + } + + /// + /// Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), + /// but this is faster. + /// + /// The double to compare to 1. + /// + /// bool - the result of the AreClose comparision. + /// + public static bool IsOne(this double value) + { + return Math.Abs(value - 1.0d) < 10.0d * DoubleEpsilon; + } + + /// + /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), + /// but this is faster. + /// + /// The double to compare to 0. + /// + /// bool - the result of the AreClose comparision. + /// + public static bool IsZero(this double value) + { + return Math.Abs(value) < 10.0d * DoubleEpsilon; + } + + /// + /// Returns whether or not two floats are "close". + /// + /// The first float to compare. + /// The second float to compare. + /// + /// bool - the result of the AreClose comparision. + /// + public static bool IsCloseTo(this float value1, float value2) + { + // In case they are Infinities or NaN (then epsilon check does not work) + if ((float.IsInfinity(value1) && + float.IsInfinity(value2)) || + (float.IsNaN(value1) && float.IsNaN(value2))) + { + return true; + } + + // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < FloatMin + var eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatMin; + var delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + /// + /// Returns whether or not the first float is less than the second float. + /// + /// The first float to compare. + /// The second float to compare. + /// + /// bool - the result of the LessThan comparision. + /// + public static bool IsLessThan(this float value1, float value2) + { + return (value1 < value2) && !value1.IsCloseTo(value2); + } + + /// + /// Returns whether or not the first float is greater than the second float. + /// + /// The first float to compare. + /// The second float to compare. + /// + /// bool - the result of the GreaterThan comparision. + /// + public static bool IsGreaterThan(this float value1, float value2) + { + return (value1 > value2) && !value1.IsCloseTo(value2); + } + + /// + /// Returns whether or not the float is "close" to 1. Same as AreClose(float, 1), + /// but this is faster. + /// + /// The float to compare to 1. + /// + /// bool - the result of the AreClose comparision. + /// + public static bool IsOne(this float value) + { + return Math.Abs(value - 1.0f) < 10.0f * FloatMin; + } + + /// + /// IsZero - Returns whether or not the float is "close" to 0. Same as AreClose(float, 0), + /// but this is faster. + /// + /// The float to compare to 0. + /// + /// bool - the result of the AreClose comparision. + /// + public static bool IsZero(this float value) + { + return Math.Abs(value) < 10.0f * FloatMin; + } + + /// + /// Compares two points for fuzzy equality. This function + /// helps compensate for the fact that double values can + /// acquire error when operated upon + /// + /// The first point to compare + /// The second point to compare + /// Whether or not the two points are equal + public static bool IsCloseTo(this Point point1, Point point2) + { + return point1.X.IsCloseTo(point2.X) && point1.Y.IsCloseTo(point2.Y); + } + + /// + /// Compares two Size instances for fuzzy equality. This function + /// helps compensate for the fact that double values can + /// acquire error when operated upon + /// + /// The first size to compare + /// The second size to compare + /// Whether or not the two Size instances are equal + public static bool IsCloseTo(this Size size1, Size size2) + { + return size1.Width.IsCloseTo(size2.Width) && size1.Height.IsCloseTo(size2.Height); + } + + /// + /// Compares two rectangles for fuzzy equality. This function + /// helps compensate for the fact that double values can + /// acquire error when operated upon + /// + /// The first rectangle to compare + /// The second rectangle to compare + /// Whether or not the two rectangles are equal + public static bool IsCloseTo(this Rect rect1, Rect rect2) + { + // If they're both empty, don't bother with the double logic. + if (rect1.IsEmpty) + { + return rect2.IsEmpty; + } + + // At this point, rect1 isn't empty, so the first thing we can test is rect2.IsEmpty, followed by property-wise compares. + return (!rect2.IsEmpty) + && rect1.X.IsCloseTo(rect2.X) && rect1.Y.IsCloseTo(rect2.Y) + && rect1.Height.IsCloseTo(rect2.Height) && rect1.Width.IsCloseTo(rect2.Width); + } + + /// + /// Rounds the given value based on the DPI scale + /// + /// Value to round + /// DPI Scale + /// The rounded value + public static double RoundLayoutValue(double value, double dpiScale) + { + double newValue; + + // If DPI == 1, don't use DPI-aware rounding. + if (!dpiScale.IsCloseTo(1.0)) + { + newValue = Math.Round(value * dpiScale) / dpiScale; + + // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. + if (double.IsNaN(newValue) || + double.IsInfinity(newValue) || + newValue.IsCloseTo(double.MaxValue)) + { + newValue = value; + } + } + else + { + newValue = Math.Round(value); + } + + return newValue; + } + + /// + /// Calculates the linear interpolated value based on the given values. + /// + /// Starting value. + /// Ending value. + /// Weight-age given to the ending value. + /// Linear interpolated value. + public static float Lerp(this float start, float end, float amount) + { + return start + ((end - start) * amount); + } + + /// + /// Verifies if this Thickness contains only valid values. The set of validity checks is passed as parameters. + /// + /// Thickness value + /// allows negative values + /// allows double.NaN + /// allows double.PositiveInfinity + /// allows double.NegativeInfinity + /// Whether or not the thickness complies to the range specified + public static bool IsValid(this Thickness thick, bool allowNegative, bool allowNaN, bool allowPositiveInfinity, bool allowNegativeInfinity) + { + if (!allowNegative) + { + if (thick.Left < 0d || thick.Right < 0d || thick.Top < 0d || thick.Bottom < 0d) + { + return false; + } + } + + if (!allowNaN) + { + if (double.IsNaN(thick.Left) || double.IsNaN(thick.Right) + || double.IsNaN(thick.Top) || double.IsNaN(thick.Bottom)) + { + return false; + } + } + + if (!allowPositiveInfinity) + { + if (double.IsPositiveInfinity(thick.Left) || double.IsPositiveInfinity(thick.Right) + || double.IsPositiveInfinity(thick.Top) || double.IsPositiveInfinity(thick.Bottom)) + { + return false; + } + } + + if (!allowNegativeInfinity) + { + if (double.IsNegativeInfinity(thick.Left) || double.IsNegativeInfinity(thick.Right) + || double.IsNegativeInfinity(thick.Top) || double.IsNegativeInfinity(thick.Bottom)) + { + return false; + } + } + + return true; + } + + /// + /// Method to add up the left and right size as width, as well as the top and bottom size as height. + /// + /// Thickness + /// Size + public static Size CollapseThickness(this Thickness thick) + { + return new Size(thick.Left + thick.Right, thick.Top + thick.Bottom); + } + + /// + /// Verifies if the Thickness contains only zero values. + /// + /// Thickness + /// Size + public static bool IsZero(this Thickness thick) + { + return thick.Left.IsZero() + && thick.Top.IsZero() + && thick.Right.IsZero() + && thick.Bottom.IsZero(); + } + + /// + /// Verifies if all the values in Thickness are same. + /// + /// Thickness + /// true if yes, otherwise false + public static bool IsUniform(this Thickness thick) + { + return thick.Left.IsCloseTo(thick.Top) + && thick.Left.IsCloseTo(thick.Right) + && thick.Left.IsCloseTo(thick.Bottom); + } + + /// + /// Converts the Thickness object to Vector4. If the Thickness object's component have values NaN, PositiveInfinity or NegativeInfinity, then Vector4.Zero will be returned. + /// + /// Thickness object + /// Vector4 + public static Vector4 ToVector4(this Thickness thickness) + { + if (thickness.IsValid(true, false, false, false)) + { + // Sanitize the component by taking only + return new Vector4( + (float)thickness.Left, + (float)thickness.Top, + (float)thickness.Right, + (float)thickness.Bottom); + } + + return Vector4.Zero; + } + + /// + /// Converts the Thickness object to Vector4. If the Thickness object contains negative components they will be converted to positive values. If the Thickness object's component have values NaN, PositiveInfinity or NegativeInfinity, then Vector4.Zero will be returned. + /// + /// Thickness object + /// Vector2 + public static Vector4 ToAbsVector4(this Thickness thickness) + { + if (thickness.IsValid(true, false, false, false)) + { + // Sanitize the component by taking only + return new Vector4( + Math.Abs((float)thickness.Left), + Math.Abs((float)thickness.Top), + Math.Abs((float)thickness.Right), + Math.Abs((float)thickness.Bottom)); + } + + return Vector4.Zero; + } + + /// + /// Gets the top left corner of the thickness structure. + /// + /// Thickness object + /// Vector2 + public static Vector2 GetOffset(this Thickness thickness) + { + return new Vector2((float)thickness.Left, (float)thickness.Top); + } + + /// + /// Verifies if the CornerRadius contains only zero values. + /// + /// CornerRadius + /// true if yes, otherwise false + public static bool IsZero(this CornerRadius corner) + { + return corner.TopLeft.IsZero() + && corner.TopRight.IsZero() + && corner.BottomRight.IsZero() + && corner.BottomLeft.IsZero(); + } + + /// + /// Verifies if the CornerRadius contains same values. + /// + /// CornerRadius + /// true if yes, otherwise false + public static bool IsUniform(this CornerRadius corner) + { + var topLeft = corner.TopLeft; + return topLeft.IsCloseTo(corner.TopRight) && + topLeft.IsCloseTo(corner.BottomLeft) && + topLeft.IsCloseTo(corner.BottomRight); + } + + /// + /// Converts the given corner value to a valid positive value. Returns zero if the corner value is Infinity or NaN or 0. + /// + /// Corner value + /// Valid Corner value + public static double ConvertToValidCornerValue(double corner) + { + if (double.IsNaN(corner) || + double.IsInfinity(corner) || + (corner < 0d)) + { + return 0d; + } + + return corner; + } + + /// + /// Converts the CornerRadius object to Vector4. If the CornerRadius object's component have values NaN, PositiveInfinity or NegativeInfinity, then Vector4.Zero will be returned. + /// + /// CornerRadius object + /// Vector4 + public static Vector4 ToVector4(this CornerRadius corner) + { + return new Vector4( + (float)corner.TopLeft, + (float)corner.TopRight, + (float)corner.BottomRight, + (float)corner.BottomLeft); + } + + /// + /// Deflates rectangle by given thickness. + /// + /// Rectangle + /// Thickness + /// Deflated Rectangle + public static Rect Deflate(this Rect rect, Thickness thick) + { + return new Rect( + rect.Left + thick.Left, + rect.Top + thick.Top, + Math.Max(0.0, rect.Width - thick.Left - thick.Right), + Math.Max(0.0, rect.Height - thick.Top - thick.Bottom)); + } + + /// + /// Inflates rectangle by given thickness. + /// + /// Rectangle + /// Thickness + /// Inflated Rectangle + public static Rect Inflate(this Rect rect, Thickness thick) + { + return new Rect( + rect.Left - thick.Left, + rect.Top - thick.Top, + Math.Max(0.0, rect.Width + thick.Left + thick.Right), + Math.Max(0.0, rect.Height + thick.Top + thick.Bottom)); + } + + /// + /// Verifies if the given brush is a SolidColorBrush and its color does not include transparency. + /// + /// Brush + /// true if yes, otherwise false + public static bool IsOpaqueSolidColorBrush(this Brush brush) + { + return (brush as SolidColorBrush)?.Color.A == 0xff; + } + + /// + /// Verifies if the given brush is the same as the otherBrush. + /// + /// Given + /// The to match it with + /// true if yes, otherwise false + public static bool IsEqualTo(this Brush brush, Brush otherBrush) + { + if (brush.GetType() != otherBrush.GetType()) + { + return false; + } + + if (ReferenceEquals(brush, otherBrush)) + { + return true; + } + + // Are both instances of SolidColorBrush + if ((brush is SolidColorBrush solidBrushA) && (otherBrush is SolidColorBrush solidBrushB)) + { + return (solidBrushA.Color == solidBrushB.Color) + && solidBrushA.Opacity.IsCloseTo(solidBrushB.Opacity); + } + + // Are both instances of LinearGradientBrush + if ((brush is LinearGradientBrush linGradBrushA) && (otherBrush is LinearGradientBrush linGradBrushB)) + { + var result = (linGradBrushA.ColorInterpolationMode == linGradBrushB.ColorInterpolationMode) + && (linGradBrushA.EndPoint == linGradBrushB.EndPoint) + && (linGradBrushA.MappingMode == linGradBrushB.MappingMode) + && linGradBrushA.Opacity.IsCloseTo(linGradBrushB.Opacity) + && (linGradBrushA.StartPoint == linGradBrushB.StartPoint) + && (linGradBrushA.SpreadMethod == linGradBrushB.SpreadMethod) + && (linGradBrushA.GradientStops.Count == linGradBrushB.GradientStops.Count); + if (!result) + { + return false; + } + + for (var i = 0; i < linGradBrushA.GradientStops.Count; i++) + { + result = (linGradBrushA.GradientStops[i].Color == linGradBrushB.GradientStops[i].Color) + && linGradBrushA.GradientStops[i].Offset.IsCloseTo(linGradBrushB.GradientStops[i].Offset); + + if (!result) + { + break; + } + } + + return result; + } + + // Are both instances of ImageBrush + if ((brush is ImageBrush imgBrushA) && (otherBrush is ImageBrush imgBrushB)) + { + var result = (imgBrushA.AlignmentX == imgBrushB.AlignmentX) + && (imgBrushA.AlignmentY == imgBrushB.AlignmentY) + && imgBrushA.Opacity.IsCloseTo(imgBrushB.Opacity) + && (imgBrushA.Stretch == imgBrushB.Stretch) + && (imgBrushA.ImageSource == imgBrushB.ImageSource); + + return result; + } + + return false; + } + + /// + /// Compares one URI with another URI. + /// + /// URI to compare with + /// URI to compare + /// true if yes, otherwise false + public static bool IsEqualTo(this Uri uri, Uri otherUri) + { + return + Uri.Compare(uri, otherUri, UriComponents.AbsoluteUri, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == 0; + } + + /// + /// Reflects point 'a' over point 'b'. + /// + /// Point to be reflected + /// Point of reflection + /// Reflected point + public static Vector2 Reflect(Vector2 a, Vector2 b) + { + // Let 'c' be the reflected point. Then point 'b' + // becomes the middle point between 'a' and 'c'. + // As per MidPoint formula, + // b.X = (a.X + c.X) / 2 and + // b.Y = (a.Y + c.Y) / 2 + // Therefore, c.X = 2 * b.X - a.X + // c.y = 2 * b.Y - a.Y + return new Vector2((2f * b.X) - a.X, (2f * b.Y) - a.Y); + } + + /// + /// Converts a Vector2 structure (x,y) to Vector3 structure (x, y, 0). + /// + /// Input Vector2 + /// Vector3 + public static Vector3 ToVector3(this Vector2 v) + { + return new Vector3(v, 0); + } + + /// + /// Verifies if the Vector4 contains only zero values. + /// + /// Vector4 + /// true if yes, otherwise false + public static bool IsZero(this Vector4 vector) + { + return vector.X.IsZero() + && vector.Y.IsZero() + && vector.Z.IsZero() + && vector.W.IsZero(); + } + + /// + /// Useful in converting the four components of Thickness or Padding to two components by taking a sum of alternate components (X & Z and Y & W). + /// + /// Vector4 + /// Vector3 + public static Vector2 Collapse(this Vector4 vector) + { + return new Vector2(vector.X + vector.Z, vector.Y + vector.W); + } + + /// + /// Useful in converting the four components of Thickness or Padding to two components by adding alternate components - (X & Z and Y & W). + /// + /// Vector4 + /// Size + public static Size ToSize(this Vector4 vector) + { + return new Size(vector.X + vector.Z, vector.Y + vector.W); + } + + /// + /// Converts the Vector4 to Thickness - Left(X), Top(Y), Right(Z), Bottom(W). + /// + /// Vector4 + /// Thickness + public static Thickness ToThickness(this Vector4 vector) + { + return new Thickness(vector.X, vector.Y, vector.Z, vector.W); + } + + /// + /// Converts the Vector4 to CornerRadius - TopLeft(X), TopRight(Y), BottomRight(Z), BottomLeft(W). + /// + /// Vector4 + /// CornerRadius + public static CornerRadius ToCornerRadius(this Vector4 vector) + { + return new CornerRadius(vector.X, vector.Y, vector.Z, vector.W); + } + + /// + /// Calculates the linear interpolated Color based on the given Color values. + /// + /// Source Color. + /// Target Color. + /// Weightage given to the target color. + /// Linear Interpolated Color. + public static Color Lerp(this Color colorFrom, Color colorTo, float amount) + { + // Convert colorFrom components to lerp-able floats + float sa = colorFrom.A, + sr = colorFrom.R, + sg = colorFrom.G, + sb = colorFrom.B; + + // Convert colorTo components to lerp-able floats + float ea = colorTo.A, + er = colorTo.R, + eg = colorTo.G, + eb = colorTo.B; + + // lerp the colors to get the difference + byte a = (byte)Math.Max(0, Math.Min(255, sa.Lerp(ea, amount))), + r = (byte)Math.Max(0, Math.Min(255, sr.Lerp(er, amount))), + g = (byte)Math.Max(0, Math.Min(255, sg.Lerp(eg, amount))), + b = (byte)Math.Max(0, Math.Min(255, sb.Lerp(eb, amount))); + + // return the new color + return Color.FromArgb(a, r, g, b); + } + + /// + /// Darkens the color by the given percentage. + /// + /// Source color. + /// Percentage to darken. Value should be between 0 and 1. + /// Color + public static Color DarkerBy(this Color color, float amount) + { + return color.Lerp(Colors.Black, amount); + } + + /// + /// Lightens the color by the given percentage. + /// + /// Source color. + /// Percentage to lighten. Value should be between 0 and 1. + /// Color + public static Color LighterBy(this Color color, float amount) + { + return color.Lerp(Colors.White, amount); + } + + /// + /// Converts the Point structure P (X,Y) to Vector3 structure + /// V (P.X, P.Y, 0); + /// + /// Point structure + /// Vector3 + public static Vector3 ToVector3(this Point p) + { + return new Vector3((float)p.X, (float)p.Y, 0f); + } + + /// + /// Calculates the best size that can fit in the destination area based on the given stretch and alignment options. + /// + /// Width of the source. + /// Height of the source. + /// Width of the destination area. + /// Height of the destination area. + /// Defines how the source should stretch to fit the destination. + /// Horizontal Alignment + /// Vertical Alignment + /// The best fitting Rectangle in the destination area. + public static Rect GetOptimumSize(double srcWidth, double srcHeight, double destWidth, double destHeight, Stretch stretch, AlignmentX horizontalAlignment, AlignmentY verticalAlignment) + { + var ratio = srcWidth / srcHeight; + var targetWidth = 0d; + var targetHeight = 0d; + + // Stretch Mode + switch (stretch) + { + case Stretch.None: + targetWidth = srcWidth; + targetHeight = srcHeight; + break; + case Stretch.Fill: + targetWidth = destWidth; + targetHeight = destHeight; + break; + case Stretch.Uniform: + // If width is greater than height + if (ratio > 1.0) + { + targetHeight = Math.Min(destWidth / ratio, destHeight); + targetWidth = targetHeight * ratio; + } + else + { + targetWidth = Math.Min(destHeight * ratio, destWidth); + targetHeight = targetWidth / ratio; + } + + break; + case Stretch.UniformToFill: + // If width is greater than height + if (ratio > 1.0) + { + targetHeight = Math.Max(destWidth / ratio, destHeight); + targetWidth = targetHeight * ratio; + } + else + { + targetWidth = Math.Max(destHeight * ratio, destWidth); + targetHeight = targetWidth / ratio; + } + + break; + } + + var left = 0d; + switch (horizontalAlignment) + { + case AlignmentX.Left: + left = 0; + break; + case AlignmentX.Center: + left = (destWidth - targetWidth) / 2.0; + break; + case AlignmentX.Right: + left = destWidth - targetWidth; + break; + } + + var top = 0d; + switch (verticalAlignment) + { + case AlignmentY.Top: + top = 0; + break; + case AlignmentY.Center: + top = (destHeight - targetHeight) / 2.0; + break; + case AlignmentY.Bottom: + top = destHeight - targetHeight; + break; + } + + return new Rect(left, top, targetWidth, targetHeight); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj b/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj index e7ccf6e9bda..99b82d778b1 100644 --- a/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj +++ b/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj @@ -35,6 +35,9 @@ - SurfaceLoader: A class that can load and draw images and other objects to Win2D surfaces and brushes. PipelineBuilder: A class that allows to build custom effects pipelines and create CompositionBrush instances from them. + + Geometry: + - CanvasPathGeometry: A class that parses Win2d Path Mini Language and converts it to CanvasGeometry, CanvasBrush, CanvasStroke, CanvasStrokeStyle or Color. UWP Toolkit Windows UI XAML brushes blur diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Visual/VisualExtensions.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Visual/VisualExtensions.cs index 85b3770dab1..1b2b323b15d 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/Visual/VisualExtensions.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Visual/VisualExtensions.cs @@ -6,7 +6,6 @@ using System.Globalization; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using Windows.UI.Composition; using Windows.UI.Xaml; using Windows.UI.Xaml.Hosting; @@ -165,11 +164,11 @@ public static Vector4 ToVector4(this string str) ///
/// A string in the format of "float, float, float, float" /// - public static Quaternion ToQuaternion(this string str) + public static unsafe Quaternion ToQuaternion(this string str) { Vector4 vector = str.ToVector4(); - return Unsafe.As(ref vector); + return *(Quaternion*)&vector; } /// diff --git a/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs b/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs index 08a6741cf50..24a7bc18801 100644 --- a/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs +++ b/Microsoft.Toolkit.Uwp.UI/Helpers/CompositionTargetHelper.cs @@ -4,7 +4,6 @@ using System; using System.Threading.Tasks; -using Microsoft.Toolkit.Diagnostics; using Windows.UI.Xaml.Media; namespace Microsoft.Toolkit.Uwp.UI.Helpers @@ -22,7 +21,10 @@ public static class CompositionTargetHelper /// Awaitable Task public static Task ExecuteAfterCompositionRenderingAsync(Action action) { - Guard.IsNotNull(action, nameof(action)); + if (action is null) + { + ThrowArgumentNullException(); + } var taskCompletionSource = new TaskCompletionSource(); @@ -45,6 +47,8 @@ void Callback(object sender, object args) } return taskCompletionSource.Task; + + static void ThrowArgumentNullException() => throw new ArgumentNullException("The parameter \"action\" must not be null."); } } } diff --git a/Microsoft.Toolkit.Uwp.UI/Microsoft.Toolkit.Uwp.UI.csproj b/Microsoft.Toolkit.Uwp.UI/Microsoft.Toolkit.Uwp.UI.csproj index edb24421aca..e7358bf1939 100644 --- a/Microsoft.Toolkit.Uwp.UI/Microsoft.Toolkit.Uwp.UI.csproj +++ b/Microsoft.Toolkit.Uwp.UI/Microsoft.Toolkit.Uwp.UI.csproj @@ -40,8 +40,8 @@ - ThemeListener: Class which listens for changes to Application Theme or High Contrast Modes and Signals an Event when they occur. UWP Toolkit Windows UI Converters XAML extensions helpers - true + true diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/DispatcherQueueTimerExtensions.cs b/Microsoft.Toolkit.Uwp/Extensions/DispatcherQueueTimerExtensions.cs similarity index 98% rename from Microsoft.Toolkit.Uwp.UI/Extensions/DispatcherQueueTimerExtensions.cs rename to Microsoft.Toolkit.Uwp/Extensions/DispatcherQueueTimerExtensions.cs index dd31f93fd2b..449892a0e16 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/DispatcherQueueTimerExtensions.cs +++ b/Microsoft.Toolkit.Uwp/Extensions/DispatcherQueueTimerExtensions.cs @@ -6,7 +6,7 @@ using System.Collections.Concurrent; using Windows.System; -namespace Microsoft.Toolkit.Uwp.UI.Extensions +namespace Microsoft.Toolkit.Uwp.Extensions { /// /// Set of extention methods for using . diff --git a/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs b/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs index f3f82615034..ff6cb38c314 100644 --- a/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs +++ b/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs @@ -5,7 +5,6 @@ using System; using System.Globalization; using System.Reflection; -using Microsoft.Toolkit.Diagnostics; using Windows.UI; using Color = Windows.UI.Color; @@ -24,7 +23,10 @@ public static class ColorHelper /// The created . public static Color ToColor(this string colorString) { - Guard.IsNotNullOrEmpty(colorString, nameof(colorString)); + if (string.IsNullOrEmpty(colorString)) + { + ThrowArgumentException(); + } if (colorString[0] == '#') { @@ -79,7 +81,7 @@ public static Color ToColor(this string colorString) return Color.FromArgb(255, r, g, b); } - default: return ThrowHelper.ThrowFormatException("The string passed in the colorString argument is not a recognized Color format."); + default: return ThrowFormatException(); } } @@ -106,7 +108,7 @@ public static Color ToColor(this string colorString) return Color.FromArgb(255, (byte)(scR * 255), (byte)(scG * 255), (byte)(scB * 255)); } - return ThrowHelper.ThrowFormatException("The string passed in the colorString argument is not a recognized Color format (sc#[scA,]scR,scG,scB)."); + return ThrowFormatException(); } var prop = typeof(Colors).GetTypeInfo().GetDeclaredProperty(colorString); @@ -116,7 +118,10 @@ public static Color ToColor(this string colorString) return (Color)prop.GetValue(null); } - return ThrowHelper.ThrowFormatException("The string passed in the colorString argument is not a recognized Color."); + return ThrowFormatException(); + + static void ThrowArgumentException() => throw new ArgumentException("The parameter \"colorString\" must not be null or empty."); + static Color ThrowFormatException() => throw new FormatException("The parameter \"colorString\" is not a recognized Color format."); } /// diff --git a/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/BaseObjectStorageHelper.cs b/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/BaseObjectStorageHelper.cs index df446a3b85d..7c1e60b0ea2 100644 --- a/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/BaseObjectStorageHelper.cs +++ b/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/BaseObjectStorageHelper.cs @@ -22,7 +22,7 @@ public abstract class BaseObjectStorageHelper : IObjectStorageHelper /// Initializes a new instance of the class, /// which can read and write data using the provided ; /// In 6.1 and older the default Serializer was based on Newtonsoft.Json. - /// To implement a based on Newtonsoft.Json or System.Text.Json see https://aka.ms/wct/storagehelper-migration + /// To implement an based on System.Text.Json, Newtonsoft.Json, or DataContractJsonSerializer see https://aka.ms/wct/storagehelper-migration /// /// The serializer to use. public BaseObjectStorageHelper(IObjectSerializer objectSerializer) diff --git a/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/SystemSerializer.cs b/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/SystemSerializer.cs index 61f247d3eed..07da5edd20f 100644 --- a/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/SystemSerializer.cs +++ b/Microsoft.Toolkit.Uwp/Helpers/ObjectStorage/SystemSerializer.cs @@ -4,7 +4,6 @@ using System; using System.Reflection; -using Microsoft.Toolkit.Diagnostics; using Windows.Storage; namespace Microsoft.Toolkit.Uwp.Helpers @@ -31,7 +30,9 @@ public T Deserialize(object value) return (T)Convert.ChangeType(value, type); } - return ThrowHelper.ThrowNotSupportedException("This serializer can only handle primitive types and strings. Please implement your own IObjectSerializer for more complex scenarios."); + return ThrowNotSupportedException(); + + static T ThrowNotSupportedException() => throw new NotSupportedException("This serializer can only handle primitive types and strings. Please implement your own IObjectSerializer for more complex scenarios."); } /// diff --git a/Microsoft.Toolkit/Microsoft.Toolkit.csproj b/Microsoft.Toolkit/Microsoft.Toolkit.csproj index b071ab13cdb..eaaa8c91009 100644 --- a/Microsoft.Toolkit/Microsoft.Toolkit.csproj +++ b/Microsoft.Toolkit/Microsoft.Toolkit.csproj @@ -13,85 +13,15 @@ UWP Toolkit Windows IncrementalLoadingCollection String Array extensions helpers - - - - - - - - - - - - - - - - - - - NETSTANDARD2_1_OR_GREATER - - - - - - - - - - NETSTANDARD2_1_OR_GREATER - - - - - - - TextTemplatingFileGenerator - Guard.Comparable.Numeric.g.cs - - - TextTemplatingFileGenerator - Guard.Collection.g.cs - - - TextTemplatingFileGenerator - ThrowHelper.Collection.g.cs - - - TextTemplatingFileGenerator - TypeInfo.g.cs - + + + - - - - - - - - True - True - Guard.Comparable.Numeric.tt - - - True - True - Guard.Collection.tt - - - True - True - ThrowHelper.Collection.tt - - - True - True - TypeInfo.ttinclude - - + + + NETSTANDARD2_1_OR_GREATER + diff --git a/SmokeTests/Microsoft.Toolkit.Diagnostics/MainPage.xaml b/SmokeTests/Microsoft.Toolkit.Diagnostics/MainPage.xaml new file mode 100644 index 00000000000..f1f78423b2d --- /dev/null +++ b/SmokeTests/Microsoft.Toolkit.Diagnostics/MainPage.xaml @@ -0,0 +1,18 @@ + + + + + +