diff --git a/build/workflow/scripts/android-uitest-run.sh b/build/workflow/scripts/android-uitest-run.sh
index ea14f3ee1..d68f083e3 100644
--- a/build/workflow/scripts/android-uitest-run.sh
+++ b/build/workflow/scripts/android-uitest-run.sh
@@ -21,7 +21,7 @@ export UNO_UITEST_SCREENSHOT_PATH=$BASE_ARTIFACTS_PATH/screenshots
export UNO_UITEST_ANDROIDAPK_PATH=$BUILD_SOURCESDIRECTORY/build/$SAMPLEAPP_ARTIFACT_NAME/$SAMPLEAPP_PACKAGE_NAME-Signed.apk
export UNO_UITEST_PROJECT=$BUILD_SOURCESDIRECTORY/src/Uno.Toolkit.UITest/Uno.Toolkit.UITest.csproj
export UNO_UITEST_ANDROID_PROJECT=$BUILD_SOURCESDIRECTORY/samples/$SAMPLE_PROJECT_NAME/$SAMPLE_PROJECT_NAME.Droid/$SAMPLE_PROJECT_NAME.Droid.csproj
-export UNO_UITEST_BINARY=$BUILD_SOURCESDIRECTORY/build/toolkit-uitest-binaries/Uno.Toolkit.UITest.dll
+export UNO_UITEST_BINARY=$BUILD_SOURCESDIRECTORY/build/toolkit-uitest-binaries-$XAML_FLAVOR_BUILD/Uno.Toolkit.UITest.dll
export UNO_UITEST_NUNIT_VERSION=$NUNIT_VERSION
export UNO_UITEST_NUGET_URL=https://dist.nuget.org/win-x86-commandline/v5.7.0/nuget.exe
export UNO_EMULATOR_INSTALLED=$BUILD_SOURCESDIRECTORY/build/.emulator_started
diff --git a/build/workflow/scripts/ios-uitest-run.sh b/build/workflow/scripts/ios-uitest-run.sh
index accf5d2c2..d0862db99 100644
--- a/build/workflow/scripts/ios-uitest-run.sh
+++ b/build/workflow/scripts/ios-uitest-run.sh
@@ -19,7 +19,7 @@ export UNO_UITEST_SCREENSHOT_PATH=$BASE_ARTIFACTS_PATH/screenshots
export UNO_UITEST_PROJECT=$BUILD_SOURCESDIRECTORY/src/Uno.Toolkit.UITest/Uno.Toolkit.UITest.csproj
export UNO_UITEST_LOGFILE=$UNO_UITEST_SCREENSHOT_PATH/nunit-log.txt
export UNO_UITEST_IOS_PROJECT=$BUILD_SOURCESDIRECTORY/samples/$SAMPLE_PROJECT_NAME/$SAMPLE_PROJECT_NAME.iOS/$SAMPLE_PROJECT_NAME.iOS.csproj
-export UNO_UITEST_BINARY=$BUILD_SOURCESDIRECTORY/build/toolkit-uitest-binaries/Uno.Toolkit.UITest.dll
+export UNO_UITEST_BINARY=$BUILD_SOURCESDIRECTORY/build/toolkit-uitest-binaries-$XAML_FLAVOR_BUILD/Uno.Toolkit.UITest.dll
export UNO_UITEST_NUNIT_VERSION=3.12.0
export UNO_UITEST_NUGET_URL=https://dist.nuget.org/win-x86-commandline/v5.7.0/nuget.exe
export UNO_ORIGINAL_TEST_RESULTS=$BUILD_SOURCESDIRECTORY/build/$UNO_TEST_RESULTS_FILE_NAME
diff --git a/build/workflow/scripts/wasm-uitest-run.sh b/build/workflow/scripts/wasm-uitest-run.sh
index 12707bfab..01d56b906 100644
--- a/build/workflow/scripts/wasm-uitest-run.sh
+++ b/build/workflow/scripts/wasm-uitest-run.sh
@@ -15,8 +15,8 @@ fi
cd $BUILD_SOURCESDIRECTORY/build/$SAMPLEAPP_ARTIFACT_NAME
-npm i chromedriver@86.0.0
-npm i puppeteer@5.3.1
+npm i chromedriver@102.0.0
+npm i puppeteer@14.1.0
# install dotnet serve / Remove as needed
dotnet tool uninstall dotnet-serve -g || true
@@ -27,12 +27,12 @@ export PATH="$PATH:$BUILD_SOURCESDIRECTORY/build/tools"
export BASE_ARTIFACTS_PATH=$BUILD_ARTIFACTSTAGINGDIRECTORY/wasm/$XAML_FLAVOR_BUILD/$UITEST_TEST_MODE_NAME
export UNO_UITEST_TARGETURI=http://localhost:5000
export UNO_UITEST_DRIVERPATH_CHROME=$BUILD_SOURCESDIRECTORY/build/$SAMPLEAPP_ARTIFACT_NAME/node_modules/chromedriver/lib/chromedriver
-export UNO_UITEST_CHROME_BINARY_PATH=$BUILD_SOURCESDIRECTORY/build/$SAMPLEAPP_ARTIFACT_NAME/node_modules/puppeteer/.local-chromium/linux-800071/chrome-linux/chrome
+export UNO_UITEST_CHROME_BINARY_PATH=$BUILD_SOURCESDIRECTORY/build/$SAMPLEAPP_ARTIFACT_NAME/node_modules/puppeteer/.local-chromium/linux-991974/chrome-linux/chrome
export UNO_UITEST_SCREENSHOT_PATH=$BASE_ARTIFACTS_PATH/screenshots
export UNO_UITEST_PLATFORM=Browser
export UNO_UITEST_CHROME_CONTAINER_MODE=true
export UNO_UITEST_PROJECT=$BUILD_SOURCESDIRECTORY/src/Uno.Toolkit.UITest/Uno.Toolkit.UITest.csproj
-export UNO_UITEST_BINARY=$BUILD_SOURCESDIRECTORY/build/toolkit-uitest-binaries/Uno.Toolkit.UITest.dll
+export UNO_UITEST_BINARY=$BUILD_SOURCESDIRECTORY/build/toolkit-uitest-binaries-$XAML_FLAVOR_BUILD/Uno.Toolkit.UITest.dll
export UNO_UITEST_LOGFILE=$BASE_ARTIFACTS_PATH/nunit-log.txt
export UNO_UITEST_WASM_PROJECT=$BUILD_SOURCESDIRECTORY/samples/$SAMPLE_PROJECT_NAME/$SAMPLE_PROJECT_NAME.Wasm/$SAMPLE_PROJECT_NAME.Wasm.csproj
export UNO_UITEST_WASM_OUTPUT_PATH=$BUILD_SOURCESDIRECTORY/samples/$SAMPLE_PROJECT_NAME/$SAMPLE_PROJECT_NAME.Wasm/bin/Release/net5.0/dist/
diff --git a/build/workflow/stage-uitests-android.yml b/build/workflow/stage-uitests-android.yml
index 5dacdc85d..85b6cdafd 100644
--- a/build/workflow/stage-uitests-android.yml
+++ b/build/workflow/stage-uitests-android.yml
@@ -120,7 +120,7 @@
- task: DownloadBuildArtifacts@0
displayName: 'Download UITest Binaries'
inputs:
- artifactName: toolkit-uitest-binaries
+ artifactName: toolkit-uitest-binaries-$(XAML_FLAVOR_BUILD)
downloadPath: '$(build.sourcesdirectory)/build'
- template: templates/dotnet-workload-install-mac.yml
diff --git a/build/workflow/stage-uitests-build.yml b/build/workflow/stage-uitests-build.yml
index acd2d00e0..0a24aa5f7 100644
--- a/build/workflow/stage-uitests-build.yml
+++ b/build/workflow/stage-uitests-build.yml
@@ -2,6 +2,14 @@ jobs:
- job: Toolkit_UITests_Build
displayName: 'Build Toolkit UI Tests'
+ strategy:
+ maxParallel: 2
+ matrix:
+ UWP:
+ XAML_FLAVOR_BUILD: UWP
+ WinUI:
+ XAML_FLAVOR_BUILD: WinUI
+
variables:
CI_Build: true
@@ -18,7 +26,7 @@ jobs:
displayName: 'Build UI Tests'
inputs:
solution: src/Uno.Toolkit.UITest/Uno.Toolkit.UITest.csproj
- msbuildArguments: /r /m /p:Configuration=Release /detailedsummary /m /bl:$(build.artifactstagingdirectory)\build.binlog
+ msbuildArguments: /r /m /p:Configuration=Release /p:FrameworkLineage=$(XAML_FLAVOR_BUILD) /detailedsummary /m /bl:$(build.artifactstagingdirectory)\build.binlog
- task: CopyFiles@2
displayName: 'Publish UITest binaries'
@@ -34,5 +42,5 @@ jobs:
retryCountOnTaskFailure: 3
inputs:
PathtoPublish: $(build.artifactstagingdirectory)
- ArtifactName: toolkit-uitest-binaries
+ ArtifactName: toolkit-uitest-binaries-$(XAML_FLAVOR_BUILD)
ArtifactType: Container
diff --git a/build/workflow/stage-uitests-ios.yml b/build/workflow/stage-uitests-ios.yml
index c55805617..be658e85c 100644
--- a/build/workflow/stage-uitests-ios.yml
+++ b/build/workflow/stage-uitests-ios.yml
@@ -125,7 +125,7 @@
- task: DownloadBuildArtifacts@0
displayName: 'Download UITest Binaries'
inputs:
- artifactName: toolkit-uitest-binaries
+ artifactName: toolkit-uitest-binaries-$(XAML_FLAVOR_BUILD)
downloadPath: '$(build.sourcesdirectory)/build'
- template: templates/dotnet-workload-install-mac.yml
diff --git a/build/workflow/stage-uitests-wasm.yml b/build/workflow/stage-uitests-wasm.yml
index ea4908eba..fea565ff1 100644
--- a/build/workflow/stage-uitests-wasm.yml
+++ b/build/workflow/stage-uitests-wasm.yml
@@ -118,7 +118,7 @@
- task: DownloadBuildArtifacts@0
displayName: 'Download UITest Binaries'
inputs:
- artifactName: toolkit-uitest-binaries
+ artifactName: toolkit-uitest-binaries-$(XAML_FLAVOR_BUILD)
downloadPath: '$(build.sourcesdirectory)/build'
- task: UseDotNet@2
diff --git a/build/workflow/templates/dotnet-workload-install-mac.yml b/build/workflow/templates/dotnet-workload-install-mac.yml
index d32824b06..eb73fb3ba 100644
--- a/build/workflow/templates/dotnet-workload-install-mac.yml
+++ b/build/workflow/templates/dotnet-workload-install-mac.yml
@@ -1,7 +1,7 @@
parameters:
- DotNetVersion: '6.0.401'
- UnoCheck_Version: '1.5.4'
- UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/34b1a60f5c1c51604b47362781969dde46979fd5/manifests/uno.ui.manifest.json'
+ DotNetVersion: '7.0.306'
+ UnoCheck_Version: '1.13.0'
+ UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/c3b289d7bb16a2a2df7f7f7f848d76fe1e74036d/manifests/uno.ui.manifest.json'
Dotnet_Root: '/usr/local/share/dotnet/'
Dotnet_Tools: '~/.dotnet/tools'
diff --git a/build/workflow/templates/dotnet-workload-install-windows.yml b/build/workflow/templates/dotnet-workload-install-windows.yml
index 3e75ceaeb..69baa8eb7 100644
--- a/build/workflow/templates/dotnet-workload-install-windows.yml
+++ b/build/workflow/templates/dotnet-workload-install-windows.yml
@@ -1,7 +1,7 @@
parameters:
- DotNetVersion: '6.0.401'
- UnoCheck_Version: '1.5.4'
- UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/34b1a60f5c1c51604b47362781969dde46979fd5/manifests/uno.ui.manifest.json'
+ DotNetVersion: '7.0.306'
+ UnoCheck_Version: '1.13.0'
+ UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/c3b289d7bb16a2a2df7f7f7f848d76fe1e74036d/manifests/uno.ui.manifest.json'
steps:
diff --git a/doc/assets/shadows-colors.png b/doc/assets/shadows-colors.png
new file mode 100644
index 000000000..f2aa84327
Binary files /dev/null and b/doc/assets/shadows-colors.png differ
diff --git a/doc/controls-styles.md b/doc/controls-styles.md
index cd7be73d8..7ec41af3f 100644
--- a/doc/controls-styles.md
+++ b/doc/controls-styles.md
@@ -10,8 +10,9 @@ The `Uno.Toolkit.UI` library adds the following controls:
- [`DrawerControl`](controls/DrawerControl.md): A container to display additional content, in a hidden pane that can be revealed using a swipe gesture, like a drawer.
- [`DrawerFlyoutPresenter`](controls/DrawerFlyoutPresenter.md): A specialized `ContentPresenter` to be used in the template of a `FlyoutPresenter` to enable gesture support.
- [`LoadingView`](controls/LoadingView.md): A control that indicates that the UI is waiting on a task to complete.
-- [`TabBar` and `TabBarItem`](controls/TabBarAndTabBarItem.md): A list of selectable items that can be used to facilitate lateral navigation within an application.
- [`NavigationBar`](controls/NavigationBar.md): A custom control that helps implement navigation logic for your application.
+- [`ShadowContainer`](controls/ShadowContainer.md): A content control allowing you to add multiple shadows to your content.
+- [`TabBar` and `TabBarItem`](controls/TabBarAndTabBarItem.md): A list of selectable items that can be used to facilitate lateral navigation within an application.
## Helpers
The `Uno.Toolkit.UI` library adds the following helper classes:
diff --git a/samples/Directory.Packages.props b/samples/Directory.Packages.props
index effb3335b..bde05a8ff 100644
--- a/samples/Directory.Packages.props
+++ b/samples/Directory.Packages.props
@@ -16,11 +16,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -48,7 +48,6 @@
-
diff --git a/samples/Uno.Toolkit.Samples/Directory.Build.props b/samples/Uno.Toolkit.Samples/Directory.Build.props
new file mode 100644
index 000000000..3d5c38df2
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+ UWP
+
+
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.Navigation.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.Navigation.cs
index a69e95708..1cc5c9030 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.Navigation.cs
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.Navigation.cs
@@ -148,7 +148,11 @@ private void AddNavigationItems(MUXC.NavigationView nv)
.OrderByDescending(x => x.SortOrder.HasValue)
.ThenBy(x => x.SortOrder)
.ThenBy(x => x.Title)
- .GroupBy(x => x.Category);
+ .GroupBy(x => x.Category)
+#if !DEBUG
+ .Where(x => x.Key != SampleCategory.Tests)
+#endif
+ ;
foreach (var category in categories.OrderBy(x => x.Key))
{
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ShadowContainerSamplePage.WinUI.xaml b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ShadowContainerSamplePage.WinUI.xaml
new file mode 100644
index 000000000..8c83612be
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ShadowContainerSamplePage.WinUI.xaml
@@ -0,0 +1,313 @@
+
+
+
+
+ #7a67f8
+ #f85977
+ #159bff
+ #67e5ad
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ShadowContainerSamplePage.WinUI.xaml.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ShadowContainerSamplePage.WinUI.xaml.cs
new file mode 100644
index 000000000..7987c7108
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ShadowContainerSamplePage.WinUI.xaml.cs
@@ -0,0 +1,47 @@
+#if IS_WINUI
+using Uno.Toolkit.Samples.Entities;
+using Uno.Toolkit.UI;
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Uno.Toolkit.Samples.Content.Controls
+{
+ [SamplePage(SampleCategory.Controls, nameof(ShadowContainer), Description = "Add many colored shadows to your controls.")]
+ public sealed partial class ShadowContainerSamplePage : Page
+ {
+ private ShadowCollection _shadows;
+
+ public ShadowContainerSamplePage()
+ {
+ this.InitializeComponent();
+
+ this.Loaded += (s, e) =>
+ {
+ var shadowContainer = SamplePageLayout.GetSampleChild(Design.Agnostic, "ShadowContainer");
+ _shadows = shadowContainer.Shadows;
+
+ var shadowsItemsControl = SamplePageLayout.GetSampleChild(Design.Agnostic, "ShadowsItemsControl");
+ shadowsItemsControl.ItemsSource = _shadows;
+ };
+ }
+
+ private void AddShadow(object sender, RoutedEventArgs e)
+ {
+ var defaultShadow = (Shadow)Resources["DefaultShadow"];
+
+ _shadows.Add(defaultShadow.Clone());
+ }
+
+ private void RemoveShadow(object sender, RoutedEventArgs e)
+ {
+ if (_shadows.Count == 0)
+ {
+ return;
+ }
+
+ _shadows.RemoveAt(_shadows.Count - 1);
+ }
+ }
+}
+#endif
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage1.xaml.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage1.xaml.cs
index b39e89c4e..b893cd7a7 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage1.xaml.cs
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage1.xaml.cs
@@ -16,19 +16,20 @@
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238
-namespace Uno.Toolkit.Samples.Content.NestedSamples;
-
-///
-/// An empty page that can be used on its own or navigated to within a Frame.
-///
-public sealed partial class FluentNavigationBarSampleNestedPage : Page
+namespace Uno.Toolkit.Samples.Content.NestedSamples
{
- public FluentNavigationBarSampleNestedPage()
- {
- this.InitializeComponent();
- }
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class FluentNavigationBarSampleNestedPage : Page
+ {
+ public FluentNavigationBarSampleNestedPage()
+ {
+ this.InitializeComponent();
+ }
- private void NavigateToNextPage(object sender, RoutedEventArgs e) => Frame.Navigate(typeof(FluentNavigationBarSampleNestedPage2));
+ private void NavigateToNextPage(object sender, RoutedEventArgs e) => Frame.Navigate(typeof(FluentNavigationBarSampleNestedPage2));
- private void NavigateBack(object sender, RoutedEventArgs e) => Shell.GetForCurrentView().BackNavigateFromNestedSample();
+ private void NavigateBack(object sender, RoutedEventArgs e) => Shell.GetForCurrentView().BackNavigateFromNestedSample();
+ }
}
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage2.xaml.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage2.xaml.cs
index 3d20135a1..e62b17c74 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage2.xaml.cs
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/NestedSamples/FluentNavigationBarSampleNestedPage2.xaml.cs
@@ -14,14 +14,15 @@
using Windows.UI.Xaml.Controls;
#endif
-namespace Uno.Toolkit.Samples.Content.NestedSamples;
-
-public sealed partial class FluentNavigationBarSampleNestedPage2 : Page
+namespace Uno.Toolkit.Samples.Content.NestedSamples
{
- public FluentNavigationBarSampleNestedPage2()
- {
- this.InitializeComponent();
- }
+ public sealed partial class FluentNavigationBarSampleNestedPage2 : Page
+ {
+ public FluentNavigationBarSampleNestedPage2()
+ {
+ this.InitializeComponent();
+ }
- private void NavigateBack(object sender, RoutedEventArgs e) => Frame.GoBack();
+ private void NavigateBack(object sender, RoutedEventArgs e) => Frame.GoBack();
+ }
}
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/TestPages/ShadowContainerTestPage.WinUI.xaml b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/TestPages/ShadowContainerTestPage.WinUI.xaml
new file mode 100644
index 000000000..893ec0f6f
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/TestPages/ShadowContainerTestPage.WinUI.xaml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/TestPages/ShadowContainerTestPage.WinUI.xaml.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/TestPages/ShadowContainerTestPage.WinUI.xaml.cs
new file mode 100644
index 000000000..a4d936f4b
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/TestPages/ShadowContainerTestPage.WinUI.xaml.cs
@@ -0,0 +1,75 @@
+#if IS_WINUI
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Uno.Toolkit.Samples.Entities;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Uno.Toolkit.Samples.Content.TestPages
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+
+ [SamplePage(SampleCategory.Tests, "ShadowContainerTest")]
+ public sealed partial class ShadowContainerTestPage : Page
+ {
+ public ShadowContainerTestPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void runButton_Click(object sender, RoutedEventArgs e)
+ {
+ statusText.Text = "Running";
+ shadowContainer.Shadows.Clear();
+
+ if (!int.TryParse(xOffsetText.Text, out var xOffset))
+ {
+ xOffset = 0;
+ }
+
+ if (!int.TryParse(yOffsetText.Text, out var yOffset))
+ {
+ yOffset = 0;
+ }
+
+ var isInner = inner.IsChecked ?? false;
+
+ shadowContainer.Shadows.Add(new UI.Shadow
+ {
+ OffsetX = xOffset,
+ OffsetY = yOffset,
+ IsInner = isInner,
+ Opacity = 1,
+ Color = Colors.Red,
+ });
+
+ statusText.Text = "Verify";
+ }
+
+ private void reset_Click(object sender, RoutedEventArgs e)
+ {
+ statusText.Text = string.Empty;
+
+ xOffsetText.Text = string.Empty;
+ yOffsetText.Text = string.Empty;
+ inner.IsChecked = false;
+
+ shadowContainer.Shadows.Clear();
+ }
+ }
+}
+#endif
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Converters.xaml b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Converters.xaml
index 5d71cd857..fbfc731f9 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Converters.xaml
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Converters.xaml
@@ -1,45 +1,39 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Converters/HexToColorConverter.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Converters/HexToColorConverter.cs
new file mode 100644
index 000000000..bcb205e19
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Converters/HexToColorConverter.cs
@@ -0,0 +1,36 @@
+using System;
+
+using Windows.UI;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml.Data;
+#else
+using Windows.UI.Xaml.Data;
+#endif
+
+namespace Uno.Toolkit.Samples.Converters
+{
+ public class HexToColorConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ return value.ToString();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ if (value is not string { Length: 9 } hex || !hex.StartsWith("#"))
+ {
+ return value;
+ }
+
+ hex = hex.Replace("#", string.Empty);
+ byte a = (byte)(System.Convert.ToUInt32(hex.Substring(0, 2), 16));
+ byte r = (byte)(System.Convert.ToUInt32(hex.Substring(2, 2), 16));
+ byte g = (byte)(System.Convert.ToUInt32(hex.Substring(4, 2), 16));
+ byte b = (byte)(System.Convert.ToUInt32(hex.Substring(6, 2), 16));
+
+ return Color.FromArgb(a, r, g, b);
+ }
+ }
+}
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Entities/SampleCategory.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Entities/SampleCategory.cs
index a62d6c8d6..1a0f41bab 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Entities/SampleCategory.cs
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Entities/SampleCategory.cs
@@ -25,5 +25,10 @@ public enum SampleCategory
/// Samples featuring static helper classes, markup-extensions, and other uncategorized stuffs.
///
Helpers,
+
+ ///
+ /// Samples uses explicitly for UI Testing purposes, not to be discoverable by default.
+ ///
+ Tests,
}
}
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
index 3c1ac4674..001dfb279 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
@@ -19,6 +19,9 @@
App.xaml
+
+ ShadowContainerSamplePage.WinUI.xaml
+
AutoLayoutPage.xaml
@@ -131,6 +134,7 @@
RuntimeTestRunner.xaml
+
ModalDialog.xaml
@@ -184,6 +188,7 @@
+
@@ -205,6 +210,10 @@
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -373,6 +382,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -519,4 +532,4 @@
-
\ No newline at end of file
+
diff --git a/samples/Uno.Toolkit.WinUI.Samples/Directory.Build.props b/samples/Uno.Toolkit.WinUI.Samples/Directory.Build.props
new file mode 100644
index 000000000..702be5604
--- /dev/null
+++ b/samples/Uno.Toolkit.WinUI.Samples/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+ WinUI
+
+
diff --git a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Droid/Uno.Toolkit.WinUI.Samples.Droid.csproj b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Droid/Uno.Toolkit.WinUI.Samples.Droid.csproj
index 3a60d85ff..eab8f3865 100644
--- a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Droid/Uno.Toolkit.WinUI.Samples.Droid.csproj
+++ b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Droid/Uno.Toolkit.WinUI.Samples.Droid.csproj
@@ -123,6 +123,10 @@
{8dc53bc5-f3f1-4334-86bb-773988675b9f}
Uno.Toolkit.RuntimeTests.WinUI
+
+ {249712f0-5e9e-40b1-b40a-df7d62db0696}
+ Uno.Toolkit.Skia.WinUI
+
{50ea1339-d38d-47f7-848a-8a827a959691}
Uno.Toolkit.WinUI
diff --git a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Skia.Gtk/Uno.Toolkit.WinUI.Samples.Skia.Gtk.csproj b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Skia.Gtk/Uno.Toolkit.WinUI.Samples.Skia.Gtk.csproj
index 5f1b60157..4b58f6425 100644
--- a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Skia.Gtk/Uno.Toolkit.WinUI.Samples.Skia.Gtk.csproj
+++ b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Skia.Gtk/Uno.Toolkit.WinUI.Samples.Skia.Gtk.csproj
@@ -59,6 +59,7 @@
+
diff --git a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Wasm/Uno.Toolkit.WinUI.Samples.Wasm.csproj b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Wasm/Uno.Toolkit.WinUI.Samples.Wasm.csproj
index 49dad42c3..b8da9b342 100644
--- a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Wasm/Uno.Toolkit.WinUI.Samples.Wasm.csproj
+++ b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Wasm/Uno.Toolkit.WinUI.Samples.Wasm.csproj
@@ -62,6 +62,7 @@
+
diff --git a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Windows.Desktop/Uno.Toolkit.WinUI.Samples.Windows.Desktop.csproj b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Windows.Desktop/Uno.Toolkit.WinUI.Samples.Windows.Desktop.csproj
index ebbab2659..d602cd5da 100644
--- a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Windows.Desktop/Uno.Toolkit.WinUI.Samples.Windows.Desktop.csproj
+++ b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Windows.Desktop/Uno.Toolkit.WinUI.Samples.Windows.Desktop.csproj
@@ -27,9 +27,15 @@
+
+
+
+
+
+
diff --git a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.iOS/Uno.Toolkit.WinUI.Samples.iOS.csproj b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.iOS/Uno.Toolkit.WinUI.Samples.iOS.csproj
index 89a43ce16..32c8bd7b8 100644
--- a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.iOS/Uno.Toolkit.WinUI.Samples.iOS.csproj
+++ b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.iOS/Uno.Toolkit.WinUI.Samples.iOS.csproj
@@ -206,6 +206,10 @@
{8dc53bc5-f3f1-4334-86bb-773988675b9f}
Uno.Toolkit.RuntimeTests.WinUI
+
+ {249712f0-5e9e-40b1-b40a-df7d62db0696}
+ Uno.Toolkit.Skia.WinUI
+
{50ea1339-d38d-47f7-848a-8a827a959691}
Uno.Toolkit.WinUI
@@ -217,4 +221,4 @@
-
+
\ No newline at end of file
diff --git a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.macOS/Uno.Toolkit.WinUI.Samples.macOS.csproj b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.macOS/Uno.Toolkit.WinUI.Samples.macOS.csproj
index 92b6d12da..682c06e8d 100644
--- a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.macOS/Uno.Toolkit.WinUI.Samples.macOS.csproj
+++ b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.macOS/Uno.Toolkit.WinUI.Samples.macOS.csproj
@@ -112,6 +112,10 @@
{8dc53bc5-f3f1-4334-86bb-773988675b9f}
Uno.Toolkit.RuntimeTests.WinUI
+
+ {249712f0-5e9e-40b1-b40a-df7d62db0696}
+ Uno.Toolkit.Skia.WinUI
+
{50ea1339-d38d-47f7-848a-8a827a959691}
Uno.Toolkit.WinUI
@@ -156,4 +160,4 @@
-
+
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 274284b16..e520c4813 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -14,7 +14,7 @@
255.255.255.255
- 11
+ latest
enable
true
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 1a9ce5c22..e1fe0d976 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -5,8 +5,11 @@
-
+
+
+
+
diff --git a/src/Uno.Toolkit.RuntimeTests/Helpers/ImageAssertHelper.cs b/src/Uno.Toolkit.RuntimeTests/Helpers/ImageAssertHelper.cs
index da690d9b9..bfc08af87 100644
--- a/src/Uno.Toolkit.RuntimeTests/Helpers/ImageAssertHelper.cs
+++ b/src/Uno.Toolkit.RuntimeTests/Helpers/ImageAssertHelper.cs
@@ -95,7 +95,7 @@ public static bool IsScreenshotSupported()
private static void AssertExpectedColor(Color expected, Color? actual)
{
- expected.Should().BeEquivalentTo(actual, config: d => d.ComparingByValue());
+ actual.Should().BeEquivalentTo(expected, config: d => d.ComparingByValue());
}
}
}
diff --git a/src/Uno.Toolkit.RuntimeTests/Tests/ShadowContainerTests.cs b/src/Uno.Toolkit.RuntimeTests/Tests/ShadowContainerTests.cs
new file mode 100644
index 000000000..673bf3f0b
--- /dev/null
+++ b/src/Uno.Toolkit.RuntimeTests/Tests/ShadowContainerTests.cs
@@ -0,0 +1,164 @@
+#if IS_WINUI
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Uno.Disposables;
+using Uno.Toolkit.RuntimeTests.Extensions;
+using Uno.Toolkit.RuntimeTests.Helpers;
+using Uno.Toolkit.RuntimeTests.Tests.TestPages;
+using Uno.Toolkit.UI;
+using Uno.UI.RuntimeTests;
+using Windows.System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Uno.UI;
+using Windows.Graphics.Imaging;
+using Windows.Storage;
+using System.IO;
+using System.Runtime.InteropServices.WindowsRuntime;
+
+
+
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media.Imaging;
+using Windows.UI.ViewManagement;
+
+namespace Uno.Toolkit.RuntimeTests.Tests
+{
+ [TestClass]
+ [RunsOnUIThread]
+ #if HAS_UNO_WINUI && !NET6_0_OR_GREATER
+ [Ignore("Disabled because Skia.Sharp doesn't support Xamarin+WinUI.")]
+ #endif
+ internal partial class ShadowContainerTests
+ {
+ [TestMethod]
+ public async Task Displays_Content()
+ {
+ if (!ImageAssertHelper.IsScreenshotSupported())
+ {
+ Assert.Inconclusive(); // System.NotImplementedException: RenderTargetBitmap is not supported on this platform.;
+ }
+
+ var greenBorder = new ShadowContainer
+ {
+ Content = new Border { Height = 200, Width = 200, Background = new SolidColorBrush(Colors.Green) }
+ };
+
+ var shadowContainer = new ShadowContainer
+ {
+ Content = greenBorder
+ };
+
+ var stackPanel = new StackPanel
+ {
+ Background = new SolidColorBrush(Colors.Yellow),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Children =
+ {
+ new Border { Height = 200, Width = 200, Background = new SolidColorBrush(Colors.Red) },
+ shadowContainer,
+ new Border { Height = 200, Width = 200, Background = new SolidColorBrush(Colors.Red) },
+ }
+ };
+
+ UnitTestsUIContentHelper.Content = stackPanel;
+
+ await UnitTestsUIContentHelper.WaitForIdle();
+ await UnitTestsUIContentHelper.WaitForLoaded(stackPanel);
+
+ var renderer = await stackPanel.TakeScreenshot();
+ await renderer.AssertColorAt(Colors.Green, 100, 300);
+ }
+
+ [TestMethod]
+ [DataRow(10, 10, false)]
+ //[DataRow(10, 10, true)]
+ //[DataRow(-10, -10, false)]
+ //[DataRow(-10, -10, true)]
+ //[DataRow(-10, 10, true)]
+ //[DataRow(10, -10, true)]
+ //[DataRow(-10, 10, false)]
+ //[DataRow(10, -10, false)]
+ public async Task Outer_Shadows(int offsetX, int offsetY, bool inner)
+ {
+ if (!ImageAssertHelper.IsScreenshotSupported())
+ {
+ Assert.Inconclusive(); // System.NotImplementedException: RenderTargetBitmap is not supported on this platform.;
+ }
+
+ var parentBorder = new Border {Height = 500, Width = 500, HorizontalAlignment = HorizontalAlignment.Center, Background = new SolidColorBrush(Colors.Yellow) };
+ var border = new Border { HorizontalAlignment = HorizontalAlignment.Center, Height = 200, Width = 200, Background = new SolidColorBrush(Colors.Green) };
+ var shadowContainer = new ShadowContainer
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Content = border,
+ };
+
+ shadowContainer.Shadows.Add(new UI.Shadow
+ {
+ Color = Colors.Red,
+ OffsetX = offsetX,
+ OffsetY = offsetY,
+ IsInner = inner,
+ Opacity = 1,
+ });
+
+ parentBorder.Child = shadowContainer;
+
+ UnitTestsUIContentHelper.Content = parentBorder;
+
+ await UnitTestsUIContentHelper.WaitForIdle();
+ await UnitTestsUIContentHelper.WaitForLoaded(shadowContainer);
+
+ var renderer = await parentBorder.TakeScreenshot();
+ var bounds = border.TransformToVisual(shadowContainer).TransformBounds(new Windows.Foundation.Rect(0, 0, border.ActualWidth, border.ActualHeight));
+
+ var xStart = offsetX < 0 ? (int)bounds.Left : (int)bounds.Right;
+ var yStart = offsetY < 0 ? (int)bounds.Top : (int)bounds.Bottom;
+
+
+ var pixels = await renderer!.GetPixelsAsync();
+ var dir = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
+
+ var c = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "image.png");
+ using (var fileStream = File.Create(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "image.png")).AsRandomAccessStream())
+ {
+ var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, fileStream);
+
+ encoder.SetPixelData(
+ BitmapPixelFormat.Bgra8,
+ BitmapAlphaMode.Ignore,
+ (uint)renderer.PixelWidth,
+ (uint)renderer.PixelHeight,
+ 96, 96,
+ pixels.ToArray()
+ );
+
+ await encoder.FlushAsync();
+ }
+
+ await renderer.AssertColorAt(Colors.Green, 100, 100);
+ await renderer.AssertColorAt(Colors.Red, 210, 100);
+
+ for (int x = 1; x <= Math.Abs(offsetX); x++)
+ {
+ x = inner ? x * -1 : x;
+ await renderer.AssertColorAt(Colors.Red, xStart + x, (int)bounds.Height / 2);
+ }
+
+ for (int y = 1; y <= Math.Abs(offsetY); y++)
+ {
+ y = inner ? y * -1 : y;
+ await renderer.AssertColorAt(Colors.Red, (int)bounds.Width / 2, yStart + y);
+ }
+ }
+ }
+}
+#endif
diff --git a/src/Uno.Toolkit.RuntimeTests/Uno.Toolkit.RuntimeTests.WinUI.csproj b/src/Uno.Toolkit.RuntimeTests/Uno.Toolkit.RuntimeTests.WinUI.csproj
index fe6746947..eeccafc88 100644
--- a/src/Uno.Toolkit.RuntimeTests/Uno.Toolkit.RuntimeTests.WinUI.csproj
+++ b/src/Uno.Toolkit.RuntimeTests/Uno.Toolkit.RuntimeTests.WinUI.csproj
@@ -7,5 +7,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/Shadow.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/Shadow.cs
new file mode 100644
index 000000000..f7f28444e
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/Shadow.cs
@@ -0,0 +1,193 @@
+using System.ComponentModel;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#endif
+
+namespace Uno.Toolkit.UI;
+
+///
+/// Dependency object representing a single shadow.
+/// Public properties are all dependency properties.
+///
+public partial class Shadow : DependencyObject, INotifyPropertyChanged
+{
+ private static readonly Windows.UI.Color DefaultColor = Windows.UI.Color.FromArgb(255, 0, 0, 0);
+
+ #region DependencyProperty: IsInner
+
+ public static readonly DependencyProperty IsInnerProperty = DependencyProperty.Register(
+ nameof(IsInner),
+ typeof(bool),
+ typeof(Shadow),
+ new(default(bool), (s, args) => OnPropertyChanged(s, nameof(IsInner))));
+
+ ///
+ /// If true, the shadow will be drawn inside the bounds of the element.
+ /// It will have the same effect as the 'inset' value in a css box-shadow.
+ ///
+ public bool IsInner
+ {
+ get => (bool)GetValue(IsInnerProperty);
+ set => SetValue(IsInnerProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: OffsetX
+
+ public static readonly DependencyProperty OffsetXProperty = DependencyProperty.Register(
+ nameof(OffsetX),
+ typeof(double),
+ typeof(Shadow),
+ new(default(double), (s, args) => OnPropertyChanged(s, nameof(OffsetX))));
+
+ ///
+ /// The X offset of the shadow.
+ ///
+ public double OffsetX
+ {
+ get => (double)GetValue(OffsetXProperty);
+ set => SetValue(OffsetXProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: OffsetY
+
+ public static readonly DependencyProperty OffsetYProperty = DependencyProperty.Register(
+ nameof(OffsetY),
+ typeof(double),
+ typeof(Shadow),
+ new(default(double), (s, args) => OnPropertyChanged(s, nameof(OffsetY))));
+
+ ///
+ /// The Y offset of the shadow.
+ ///
+ public double OffsetY
+ {
+ get => (double)GetValue(OffsetYProperty);
+ set => SetValue(OffsetYProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: Color
+
+ public static readonly DependencyProperty ColorProperty = DependencyProperty.Register(
+ nameof(Color),
+ typeof(Windows.UI.Color),
+ typeof(Shadow),
+ new(DefaultColor, (s, args) => OnPropertyChanged(s, nameof(Color))));
+
+ ///
+ /// The color of the shadow.
+ /// It will be multiplied by the opacity property before rendering.
+ ///
+ public Windows.UI.Color Color
+ {
+ get => (Windows.UI.Color)GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: Opacity
+
+ public static readonly DependencyProperty OpacityProperty = DependencyProperty.Register(
+ nameof(Opacity),
+ typeof(double),
+ typeof(Shadow),
+ new(default(double), (s, args) => OnPropertyChanged(s, nameof(Opacity))));
+
+ ///
+ /// The opacity of the shadow.
+ ///
+ public double Opacity
+ {
+ get => (double)GetValue(OpacityProperty);
+ set => SetValue(OpacityProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: BlurRadius
+
+ public static readonly DependencyProperty BlurRadiusProperty = DependencyProperty.Register(
+ nameof(BlurRadius),
+ typeof(double),
+ typeof(Shadow),
+ new(default(double), (s, args) => OnPropertyChanged(s, nameof(BlurRadius))));
+
+ ///
+ /// The radius of the blur that will be applied to the shadow [0..100].
+ ///
+ public double BlurRadius
+ {
+ get => (double)GetValue(BlurRadiusProperty);
+ set => SetValue(BlurRadiusProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: Spread
+
+ public static readonly DependencyProperty SpreadProperty = DependencyProperty.Register(
+ nameof(Spread),
+ typeof(double),
+ typeof(Shadow),
+ new(default(double), (s, args) => OnPropertyChanged(s, nameof(Spread))));
+
+ ///
+ /// The spread will inflate or deflate (if negative) the control shadow size before applying the blur.
+ ///
+ public double Spread
+ {
+ get => (double)GetValue(SpreadProperty);
+ set => SetValue(SpreadProperty, value);
+ }
+
+ #endregion
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ internal static bool IsShadowProperty(string propertyName)
+ {
+ return propertyName == nameof(OffsetX) || propertyName == nameof(OffsetY) ||
+ propertyName == nameof(IsInner) ||
+ propertyName == nameof(Color) ||
+ propertyName == nameof(Opacity) ||
+ propertyName == nameof(BlurRadius) ||
+ propertyName == nameof(Spread);
+ }
+
+ internal static bool IsShadowSizeProperty(string propertyName)
+ {
+ return propertyName == nameof(OffsetX) || propertyName == nameof(OffsetY) ||
+ propertyName == nameof(IsInner) ||
+ propertyName == nameof(BlurRadius) ||
+ propertyName == nameof(Spread);
+ }
+
+ private static void OnPropertyChanged(object dependencyObject, string propertyName)
+ {
+ ((Shadow)dependencyObject).PropertyChanged?.Invoke(dependencyObject, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public override string ToString() =>
+ $"{{ IsInner: {{{IsInner}}}, Offset: {{{OffsetX}, {OffsetY}}} Color: {{A={Color.A}, R={Color.R}, G={Color.G}, B={Color.B}}}, Opacity: {Opacity}, BlurRadius: {BlurRadius}, Spread: {Spread} }}";
+
+ public string ToKey() =>
+ string.Join(",", IsInner, OffsetX, OffsetY, Color.ToString(), Opacity, BlurRadius, Spread);
+
+ public Shadow Clone()
+ {
+ return new Shadow
+ {
+ IsInner = IsInner,
+ OffsetX = OffsetX,
+ OffsetY = OffsetY,
+ Color = Color,
+ Opacity = Opacity,
+ BlurRadius = BlurRadius,
+ Spread = Spread
+ };
+ }
+}
diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowCollection.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowCollection.cs
new file mode 100644
index 000000000..c3ce9a5d0
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowCollection.cs
@@ -0,0 +1,13 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Uno.Toolkit.UI;
+
+public class ShadowCollection : ObservableCollection
+{
+ public bool HasInnerShadow() => this.Any(s => s.IsInner);
+
+ public string ToKey(double width, double height, Windows.UI.Color? contentBackground)
+ => $"w{width},h{height}" + (contentBackground.HasValue ? $",cb{contentBackground.Value}:" : ":") +
+ string.Join("/", this.Select(x => x.ToKey()));
+}
diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Paint.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Paint.cs
new file mode 100644
index 000000000..08ba8f901
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Paint.cs
@@ -0,0 +1,382 @@
+using System;
+using System.Linq;
+
+using SkiaSharp;
+
+using Windows.UI;
+
+using Microsoft.Extensions.Logging;
+using Uno.Extensions;
+using Uno.Logging;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Controls;
+using SkiaSharp.Views.Windows;
+#else
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Controls;
+using SkiaSharp.Views.UWP;
+#endif
+
+namespace Uno.Toolkit.UI;
+
+public partial class ShadowContainer
+{
+ private static readonly ILogger _logger = typeof(ShadowContainer).Log();
+
+ private record ShadowInfos(double Width, double Height, bool IsInner, double BlurRadius, double Spread, double X, double Y, Color color)
+ {
+ public static readonly ShadowInfos Empty = new ShadowInfos(0, 0, false, 0, 0, 0, 0, new Color());
+
+ public static ShadowInfos From(Shadow shadow, double width, double height)
+ {
+ return new ShadowInfos(
+ width,
+ height,
+ shadow.IsInner,
+ shadow.BlurRadius,
+ shadow.Spread,
+ shadow.OffsetX,
+ shadow.OffsetY,
+ Color.FromArgb((byte)(shadow.Color.A * shadow.Opacity), shadow.Color.R, shadow.Color.G, shadow.Color.B));
+ }
+ }
+
+ private record struct SKShadow(
+ bool IsInner,
+ float OffsetX,
+ float OffsetY,
+ float BlurSigma,
+ SKColor Color,
+ float Spread,
+ float ContentWidth,
+ float ContentHeight,
+ float CornerRadius)
+ {
+ public static SKShadow From(Shadow shadow, float width, float height, float cornerRadius, float pixelRatio)
+ {
+ float blurRadius = (float)shadow.BlurRadius * pixelRatio;
+ // Blur sigma conversion taken from flutter source code
+ float blurSigma = blurRadius > 0 ? blurRadius * 0.57735f + 0.5f : 0f;
+
+ // Can't use ToSKColor() or we end up with a weird compilation error asking us to reference System.Drawing
+ Color windowsUiColor = shadow.Color;
+ var color = ToSkiaColor(windowsUiColor);
+ color = color.WithAlpha((byte)(color.Alpha * shadow.Opacity));
+
+ return new SKShadow(
+ shadow.IsInner,
+ (float)shadow.OffsetX * pixelRatio,
+ (float)shadow.OffsetY * pixelRatio,
+ blurSigma,
+ color,
+ (float)shadow.Spread * pixelRatio,
+ width,
+ height,
+ cornerRadius);
+ }
+ }
+
+ private ShadowInfos[] _shadowInfoArray = new[] { ShadowInfos.Empty };
+ private float _currentPixelRatio;
+ private Color? _currentContentBackgroundColor;
+
+ private bool NeedsPaint(double width, double height, float pixelRatio, out bool pixelRatioChanged)
+ {
+ var shadows = Shadows ?? new ShadowCollection();
+ var newShadowInfos = shadows.Select(s => ShadowInfos.From(s, width, height)).ToArray();
+
+ pixelRatioChanged = false;
+
+ bool needsPaint = !newShadowInfos.SequenceEqual(_shadowInfoArray);
+ _shadowInfoArray = newShadowInfos;
+
+ if (pixelRatio != _currentPixelRatio)
+ {
+ _currentPixelRatio = pixelRatio;
+ pixelRatioChanged = needsPaint = true;
+ }
+
+ return needsPaint;
+ }
+
+#if false // ANDROID (see comment in ShadowContainer.cs)
+ private void OnSurfacePainted(object? sender, SKPaintGLSurfaceEventArgs e)
+ {
+ if (!_notOpaqueSet && ((ViewGroup)_shadowHost).GetChildAt(0) is TextureView openGlTexture)
+ {
+ openGlTexture.SetOpaque(false);
+ _notOpaqueSet = true;
+ }
+#else
+ private void OnSurfacePainted(object? sender, SKPaintSurfaceEventArgs e)
+ {
+#endif
+ if (_shadowHost == null || _currentContent is not { ActualHeight: > 0, ActualWidth: > 0 })
+ {
+ return;
+ }
+
+ var surface = e.Surface;
+ var surfaceWidth = e.Info.Width;
+ var surfaceHeight = e.Info.Height;
+
+ float pixelRatio = surfaceWidth / (float)_shadowHost.Width;
+ double width = _currentContent.ActualWidth;
+ double height = _currentContent.ActualHeight;
+
+ if (!NeedsPaint(width, height, pixelRatio, out bool pixelRatioChanged))
+ {
+ return;
+ }
+
+ var canvas = surface.Canvas;
+ canvas.Clear(SKColors.Transparent);
+ canvas.Save();
+
+ if (Shadows is not { Count: > 0 } shadows)
+ {
+ return;
+ }
+
+ // If there is any inner shadow, we need to:
+ // 1. Get the background color from the content
+ // 2. Set the content background to transparent
+ // 3. Draw the content background with skia underneath inner shadows
+ bool hasInnerShadow = shadows.HasInnerShadow();
+ if (hasInnerShadow)
+ {
+ // Will set the content background to transparent if needed
+ if (_currentContentBackgroundColor == null && ProcessContentBackgroundIfNeeded(out var contentBackgroundWinUIColor))
+ {
+ _currentContentBackgroundColor = contentBackgroundWinUIColor;
+ }
+ }
+ else if (_currentContentBackgroundColor.HasValue)
+ {
+ // Means that there were inner shadows, and they have been removed: restore content background
+ TrySetContentBackground(new SolidColorBrush(_currentContentBackgroundColor.Value));
+ _currentContentBackgroundColor = null;
+ }
+
+ string shadowsKey = shadows.ToKey(width, height, _currentContentBackgroundColor);
+ if (Cache.TryGetValue(shadowsKey, out var shadowsImage))
+ {
+ if (pixelRatioChanged)
+ {
+ // Monitor pixel density changed, need to remove cached image
+ Cache.Remove(shadowsKey);
+ }
+ else
+ {
+ canvas.DrawImage(shadowsImage, SKPoint.Empty);
+ canvas.Restore();
+ return;
+ }
+ }
+
+ float childWidth = (float)width * pixelRatio;
+ float childHeight = (float)height * pixelRatio;
+
+ float diffWidthSurfaceChild = surfaceWidth - childWidth;
+ float diffHeightSurfaceChild = surfaceHeight - childHeight;
+ canvas.Translate(diffWidthSurfaceChild / 2, diffHeightSurfaceChild / 2);
+
+ using var paint = new SKPaint();
+ paint.IsAntialias = true;
+
+ float cornerRadius = (float)_cornerRadius.BottomRight * pixelRatio;
+
+ foreach (var shadow in shadows.Where(s => !s.IsInner))
+ {
+ var skShadow = SKShadow.From(shadow, childWidth, childHeight, cornerRadius, pixelRatio);
+
+ DrawDropShadow(canvas, paint, skShadow);
+ }
+
+ // Always draw inner shadows on top of the drop shadows
+ if (hasInnerShadow)
+ {
+ var contentShape = new SKRoundRect(new SKRect(0, 0, childWidth, childHeight), cornerRadius);
+ canvas.ClipRoundRect(contentShape, antialias: true);
+
+ // Draw the content background first
+ if (_currentContentBackgroundColor.HasValue)
+ {
+ var contentBackgroundColor = ToSkiaColor(_currentContentBackgroundColor.Value);
+ DrawContentBackground(canvas, contentBackgroundColor, contentShape);
+ }
+
+ // Then we draw the inner shadows
+ foreach (var shadow in shadows.Where(s => s.IsInner))
+ {
+ var skShadow = SKShadow.From(shadow, childWidth, childHeight, cornerRadius, pixelRatio);
+
+ DrawInnerShadow(canvas, paint, skShadow);
+ }
+ }
+
+ canvas.Restore();
+
+ if (!_shadowPropertyChanged)
+ {
+ // If a property has changed dynamically, we don't want to cache the updated shadows
+ Cache.AddOrUpdate(shadowsKey, surface.Snapshot());
+ }
+
+ _shadowPropertyChanged = false;
+ }
+
+ private bool ProcessContentBackgroundIfNeeded(out Color? contentBackgroundColor)
+ {
+ contentBackgroundColor = null;
+ if (TryGetContentBackground(out var background))
+ {
+ if (background is not SolidColorBrush backgroundColorBrush)
+ {
+ throw new NotSupportedException("[ShadowContainer] Unsupported Background brush: when using inner shadows the only supported brush type for the Background property is SolidBrushColor");
+ }
+
+ if (backgroundColorBrush.Color != Color.FromArgb(0, 0, 0, 0))
+ {
+ contentBackgroundColor = backgroundColorBrush.Color;
+ }
+
+ TrySetContentBackground(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)));
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void DrawContentBackground(SKCanvas canvas, SKColor contentBackgroundColor, SKRoundRect childShape)
+ {
+ using var backgroundPaint = new SKPaint
+ {
+ Color = contentBackgroundColor,
+ Style = SKPaintStyle.Fill,
+ };
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.Debug(
+ $"[ShadowContainer] DrawContentBackground => color: {backgroundPaint.Color}");
+ }
+ canvas.DrawRoundRect(childShape, backgroundPaint);
+ }
+
+ private static void DrawDropShadow(SKCanvas canvas, SKPaint paint, SKShadow shadow)
+ {
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = shadow.Color;
+ paint.ImageFilter = SKImageFilter.CreateBlur(shadow.BlurSigma, shadow.BlurSigma);
+ paint.MaskFilter = null;
+ paint.StrokeWidth = 0;
+
+ // Two other ways to create shadows
+ // 1. Mask filter
+ // x = 0;
+ // y = 0;
+ // paint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, blurSigma);
+ // 2. DropShadow
+ // paint.ImageFilter = SKImageFilter.CreateDropShadowOnly(
+ // 0,
+ // 0,
+ // blurSigma,
+ // blurSigma,
+ // color);
+
+ var shadowShape = new SKRoundRect(
+ new SKRect(
+ shadow.OffsetX,
+ shadow.OffsetY,
+ shadow.OffsetX + shadow.ContentWidth,
+ shadow.OffsetY + shadow.ContentHeight),
+ shadow.CornerRadius);
+ shadowShape.Inflate(shadow.Spread, shadow.Spread);
+ canvas.DrawRoundRect(shadowShape, paint);
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.Debug(
+ $"[ShadowContainer] DrawDropShadow => x: {shadow.OffsetX}, y: {shadow.OffsetY}, width: {shadow.ContentWidth}, height: {shadow.ContentHeight}");
+ }
+ }
+
+ private static void DrawInnerShadow(SKCanvas canvas, SKPaint paint, SKShadow shadow)
+ {
+ float strokeWidthX = Math.Abs(shadow.OffsetX);
+ float strokeWidthY = Math.Abs(shadow.OffsetY);
+ float strokeWidth = Math.Max(Math.Max(strokeWidthX, strokeWidthY), shadow.BlurSigma) + shadow.Spread * 2;
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = shadow.Color;
+ paint.StrokeWidth = strokeWidth * 2;
+ paint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, shadow.BlurSigma);
+ paint.ImageFilter = null;
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.Debug(
+ $"[ShadowContainer] DrawInnerShadow => strokeWidth: {paint.StrokeWidth}, cornerRadius: {shadow.CornerRadius}, x: {shadow.OffsetX}, y: {shadow.OffsetY}, width: {shadow.ContentWidth}, height: {shadow.ContentHeight}");
+ }
+
+ var shadowShape = new SKRoundRect(
+ new SKRect(
+ 0,
+ 0,
+ shadow.ContentWidth + (paint.StrokeWidth),
+ shadow.ContentHeight + (paint.StrokeWidth)),
+ shadow.CornerRadius);
+
+ shadowShape.Deflate(shadow.Spread, shadow.Spread);
+
+ shadowShape.Offset(shadow.OffsetX - strokeWidth, shadow.OffsetY - strokeWidth);
+ canvas.DrawRoundRect(shadowShape, paint);
+ }
+
+ private bool TryGetContentBackground(out Brush? background)
+ {
+ if (_currentContent == null)
+ {
+ background = null;
+ return false;
+ }
+
+ background = _currentContent switch
+ {
+ Control control => control.Background,
+ Panel panel => panel.Background,
+ Border border => border.Background,
+ _ => null,
+ };
+
+ return background != null;
+ }
+
+ private bool TrySetContentBackground(SolidColorBrush background)
+ {
+ switch (_currentContent)
+ {
+ case Control control:
+ control.Background = background;
+ break;
+ case Panel panel:
+ panel.Background = background;
+ break;
+ case Border border:
+ border.Background = background;
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ private static SKColor ToSkiaColor(Color windowsUiColor)
+ {
+ return new SKColor(windowsUiColor.R, windowsUiColor.G, windowsUiColor.B, windowsUiColor.A);
+ }
+}
diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Properties.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Properties.cs
new file mode 100644
index 000000000..42fa56917
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Properties.cs
@@ -0,0 +1,151 @@
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using Uno.Disposables;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#endif
+
+namespace Uno.Toolkit.UI;
+
+public partial class ShadowContainer : ContentControl
+{
+ private readonly SerialDisposable _shadowsCollectionChanged = new();
+ private readonly SerialDisposable _shadowPropertiesChanged = new();
+ private readonly SerialDisposable _cornerRadiusChanged = new();
+ private readonly CompositeDisposable _activeShadowRegistrations = new CompositeDisposable();
+
+ #region DependencyProperty: Shadows
+
+ public static readonly DependencyProperty ShadowsProperty =
+ DependencyProperty.Register(
+ nameof(Shadows),
+ typeof(ShadowCollection),
+ typeof(ShadowContainer),
+ new(new ShadowCollection(), OnShadowsChanged));
+
+ ///
+ /// The collection of shadows that will be displayed under your control.
+ /// A ShadowCollection can be stored in a resource dictionary to have a consistent style through your app.
+ /// The ShadowCollection implements INotifyCollectionChanged.
+ ///
+ public ShadowCollection Shadows
+ {
+ get => (ShadowCollection)GetValue(ShadowsProperty);
+ set => SetValue(ShadowsProperty, value);
+ }
+
+ #endregion
+
+ // True if a shadow property has changed dynamically or if we add or removed a shadow
+ private bool _shadowPropertyChanged;
+
+ private static void OnShadowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ShadowContainer shadowContainer)
+ {
+ shadowContainer.UpdateShadows();
+ }
+ }
+
+ private void OnShadowCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ for (int i = 0; i < e.NewItems!.Count; i++)
+ {
+ OnShadowInserted((Shadow)e.NewItems[i]!);
+ }
+
+ OnShadowSizeChanged();
+ InvalidateFromShadowPropertyChange();
+
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ for (int i = 0; i < e.OldItems!.Count; i++)
+ {
+ OnShadowRemoved((Shadow)e.OldItems[i]!);
+ }
+
+ OnShadowSizeChanged();
+ InvalidateFromShadowPropertyChange();
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ OnShadowSizeChanged();
+ InvalidateFromShadowPropertyChange();
+ break;
+ }
+ }
+
+ private void UpdateShadows()
+ {
+ _shadowsCollectionChanged.Disposable = null;
+ _shadowPropertiesChanged.Disposable = null;
+
+ if (Shadows is not { } shadows)
+ {
+ return;
+ }
+
+ foreach (var shadow in shadows)
+ {
+ _activeShadowRegistrations.Add(() => shadow.PropertyChanged -= ShadowPropertyChanged);
+ shadow.PropertyChanged += ShadowPropertyChanged;
+ }
+
+ _shadowsCollectionChanged.Disposable = Disposable.Create(() => shadows.CollectionChanged -= OnShadowCollectionChanged);
+ shadows.CollectionChanged += OnShadowCollectionChanged;
+
+ _shadowPropertiesChanged.Disposable = _activeShadowRegistrations;
+
+ OnShadowSizeChanged();
+ _shadowHost?.Invalidate();
+ }
+
+ private void OnShadowInserted(Shadow shadow)
+ {
+ _activeShadowRegistrations.Add(() => shadow.PropertyChanged -= ShadowPropertyChanged);
+ shadow.PropertyChanged += ShadowPropertyChanged;
+ }
+
+ private void OnShadowRemoved(Shadow shadow)
+ {
+ shadow.PropertyChanged -= ShadowPropertyChanged;
+ }
+
+ private void ShadowPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName is null)
+ {
+ return;
+ }
+
+ if (UI.Shadow.IsShadowSizeProperty(e.PropertyName))
+ {
+ OnShadowSizeChanged();
+ }
+
+ InvalidateFromShadowPropertyChange();
+ }
+
+ private void OnShadowSizeChanged()
+ {
+ if (_currentContent != null && _currentContent.ActualWidth > 0 && _currentContent.ActualHeight > 0)
+ {
+ UpdateCanvasSize(_currentContent.ActualWidth, _currentContent.ActualHeight, Shadows);
+ }
+ }
+
+ private void InvalidateFromShadowPropertyChange()
+ {
+ _shadowPropertyChanged = true;
+ _shadowHost?.Invalidate();
+ }
+}
diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs
new file mode 100644
index 000000000..901e993fb
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Linq;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using SkiaSharp.Views.Windows;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using SkiaSharp.Views.UWP;
+#endif
+
+#if __ANDROID__
+using Android.Views;
+#endif
+
+namespace Uno.Toolkit.UI;
+
+///
+/// Provides the possibility to add many-colored shadows to its content.
+///
+///
+/// For now it renders badly on WASM due to a bug on the wasm skiasharp construction of the SKXamlCanvas.
+/// It should be fixed when this PR will be merged: https://github.com/mono/SkiaSharp/pull/2443
+///
+[TemplatePart(Name = nameof(PART_Canvas), Type = typeof(Canvas))]
+public partial class ShadowContainer : ContentControl
+{
+ private const string PART_Canvas = "PART_Canvas";
+
+ private Canvas? _canvas;
+
+#if false // ANDROID (see comment below)
+ private readonly SKSwapChainPanel _shadowHost;
+ private bool _notOpaqueSet = false;
+#else
+ private SKXamlCanvas? _shadowHost;
+#endif
+
+ private static readonly ShadowsCache Cache = new ShadowsCache();
+
+ private FrameworkElement? _currentContent;
+
+ private CornerRadius _cornerRadius;
+
+ public ShadowContainer()
+ {
+#if HAS_UNO_WINUI && !NET6_0_OR_GREATER
+ throw new NotSupportedException("ShadowContainer doesn't support Xamarin + WinUI considering moving to .NET6 or greater.");
+#else
+ DefaultStyleKey = typeof(ShadowContainer);
+
+ _cornerRadius = new CornerRadius(0);
+
+ Loaded += ShadowContainerLoaded;
+ Unloaded += ShadowContainerUnloaded;
+#endif
+ }
+
+ private void ShadowContainerUnloaded(object sender, RoutedEventArgs e)
+ {
+ RevokeListeners();
+ }
+
+ private void ShadowContainerLoaded(object sender, RoutedEventArgs e)
+ {
+ UpdateShadows();
+ }
+
+ private void RevokeListeners()
+ {
+ _shadowsCollectionChanged.Disposable = null;
+ _shadowPropertiesChanged.Disposable = null;
+ _cornerRadiusChanged.Disposable = null;
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ _canvas = GetTemplateChild(nameof(PART_Canvas)) as Canvas;
+
+
+#if false // ANDROID: We keep that as a reference cause it would be better to use the hardware-accelerated version
+ var skiaCanvas = new SKSwapChainPanel();
+ skiaCanvas.PaintSurface += OnSurfacePainted;
+#else
+ var skiaCanvas = new SKXamlCanvas();
+ skiaCanvas.PaintSurface += OnSurfacePainted;
+#endif
+
+#if __IOS__ || __MACCATALYST__
+ skiaCanvas.Opaque = false;
+#endif
+
+ _shadowHost = skiaCanvas;
+ _canvas?.Children.Insert(0, _shadowHost!);
+
+ base.OnApplyTemplate();
+ }
+
+ ///
+ protected override void OnContentChanged(object oldContent, object newContent)
+ {
+ _cornerRadiusChanged.Disposable = null;
+
+ if (oldContent is FrameworkElement oldElement)
+ {
+ _canvas?.Children.Remove(oldElement);
+ oldElement.SizeChanged -= OnContentSizeChanged;
+ }
+
+ if (newContent is FrameworkElement newElement)
+ {
+ _currentContent = newElement;
+ _currentContent.SizeChanged += OnContentSizeChanged;
+
+ if (TryGetCornerRadius(newElement, out var cornerRadius))
+ {
+ var cornerRadiusProperty = newElement switch
+ {
+ Grid _ => Grid.CornerRadiusProperty,
+ StackPanel _ => StackPanel.CornerRadiusProperty,
+ ContentPresenter _ => ContentPresenter.CornerRadiusProperty,
+ Border _ => Border.CornerRadiusProperty,
+ Control _ => Control.CornerRadiusProperty,
+ RelativePanel _ => RelativePanel.CornerRadiusProperty,
+ _ => default,
+
+ };
+
+ if (cornerRadiusProperty != null)
+ {
+ _cornerRadiusChanged.Disposable = newElement.RegisterDisposablePropertyChangedCallback(
+ cornerRadiusProperty,
+ (s, dp) => OnCornerRadiusChanged(s, dp)
+ );
+ }
+ }
+
+ _cornerRadius = cornerRadius;
+ }
+
+ _shadowHost?.Invalidate();
+ base.OnContentChanged(oldContent, newContent);
+ }
+
+ private void OnCornerRadiusChanged(DependencyObject sender, DependencyProperty dp)
+ {
+ if (_currentContent is { })
+ {
+ if (TryGetCornerRadius(_currentContent, out var cornerRadius))
+ {
+ _cornerRadius = cornerRadius;
+ _shadowHost?.Invalidate();
+ }
+ }
+ }
+
+ private static bool TryGetCornerRadius(FrameworkElement element, out CornerRadius cornerRadius)
+ {
+ CornerRadius? localCornerRadius = element switch
+ {
+ Control control => control.CornerRadius,
+ StackPanel stackPanel => stackPanel.CornerRadius,
+ RelativePanel relativePanel => relativePanel.CornerRadius,
+ Grid grid => grid.CornerRadius,
+ Border border => border.CornerRadius,
+ _ => VisualTreeHelperEx.TryGetDpValue(element, "CornerRadius", out var value) ? value : default(CornerRadius?),
+ };
+
+ cornerRadius = localCornerRadius ?? new CornerRadius(0);
+ return localCornerRadius != null;
+ }
+
+ private void OnContentSizeChanged(object sender, SizeChangedEventArgs args)
+ {
+ if (args.NewSize.Width > 0 && args.NewSize.Height > 0)
+ {
+ UpdateCanvasSize(args.NewSize.Width, args.NewSize.Height, Shadows);
+ _shadowHost?.Invalidate();
+ }
+ }
+
+ private void UpdateCanvasSize(double childWidth, double childHeight, ShadowCollection? shadows)
+ {
+ if (_currentContent == null || _canvas == null || _shadowHost == null)
+ {
+ return;
+ }
+
+ double absoluteMaxOffsetX = 0;
+ double absoluteMaxOffsetY = 0;
+ double maxBlurRadius = 0;
+ double maxSpread = 0;
+
+ if (shadows?.Any() == true)
+ {
+ absoluteMaxOffsetX = shadows.Max(s => Math.Abs(s.OffsetX));
+ absoluteMaxOffsetY = shadows.Max(s => Math.Abs(s.OffsetY));
+ maxBlurRadius = shadows.Max(s => s.BlurRadius);
+ maxSpread = shadows.Max(s => s.Spread);
+ }
+
+ _canvas.Height = childHeight;
+ _canvas.Width = childWidth;
+#if __ANDROID__ || __IOS__
+ _canvas.GetDispatcherCompat().Schedule(() => _canvas.InvalidateMeasure());
+#endif
+ double newHostHeight = childHeight + maxBlurRadius * 2 + absoluteMaxOffsetY * 2 + maxSpread * 2;
+ double newHostWidth = childWidth + maxBlurRadius * 2 + absoluteMaxOffsetX * 2 + maxSpread * 2;
+ _shadowHost.Height = newHostHeight;
+ _shadowHost.Width = newHostWidth;
+
+ double diffWidthShadowHostChild = newHostWidth - childWidth;
+ double diffHeightShadowHostChild = newHostHeight - childHeight;
+
+ float left = (float)(-diffWidthShadowHostChild / 2 + _currentContent.Margin.Left);
+ float top = (float)(-diffHeightShadowHostChild / 2 + _currentContent.Margin.Top);
+
+ Canvas.SetLeft(_shadowHost, left);
+ Canvas.SetTop(_shadowHost, top);
+ }
+}
diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowsCache.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowsCache.cs
new file mode 100644
index 000000000..62e2e6f66
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowsCache.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Concurrent;
+
+using SkiaSharp;
+
+namespace Uno.Toolkit.UI;
+public class ShadowsCache
+{
+ private class CacheBucket
+ {
+ public CacheBucket(SKImage bitmap)
+ {
+ Bitmap = bitmap;
+ AddHit();
+ }
+
+ public SKImage Bitmap { get; }
+
+ public int Hit { get; private set; }
+
+ public DateTime LastHit { get; private set; }
+
+ public void AddHit()
+ {
+ Hit++;
+ LastHit = DateTime.UtcNow;
+ }
+ }
+
+ private readonly ConcurrentDictionary _shadowsCache = new ConcurrentDictionary();
+
+ public void AddOrUpdate(string key, SKImage image)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ShadowsCache] AddOrUpdate => key: {key}");
+
+ var bucket = _shadowsCache.AddOrUpdate(
+ key,
+ (key) =>
+ {
+ System.Diagnostics.Debug.WriteLine($"[ShadowsCache] inserting new shadow in cache");
+ return new CacheBucket(image);
+ },
+ (key, existing) =>
+ {
+ existing.AddHit();
+ System.Diagnostics.Debug.WriteLine($"[ShadowsCache] shadow image found, {existing.Hit} hits, last one at {existing.LastHit:T}");
+ return existing;
+ });
+ }
+
+ public bool TryGetValue(string key, out SKImage? image)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ShadowsCache] TryGet => key: {key}");
+
+ if (_shadowsCache.TryGetValue(key, out var bucket))
+ {
+ bucket.AddHit();
+ image = bucket.Bitmap;
+ System.Diagnostics.Debug.WriteLine($"[ShadowsCache] shadow image found, {bucket.Hit} hits, last one at {bucket.LastHit:T}");
+ return true;
+ }
+
+ image = null;
+ return false;
+ }
+
+ public bool Remove(string key)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ShadowsCache] Remove => key: {key}");
+
+ return _shadowsCache.TryRemove(key, out var _);
+ }
+}
diff --git a/src/Uno.Toolkit.Skia.WinUI/Themes/Generic.xaml b/src/Uno.Toolkit.Skia.WinUI/Themes/Generic.xaml
new file mode 100644
index 000000000..88fb5c800
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Themes/Generic.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
diff --git a/src/Uno.Toolkit.Skia.WinUI/Uno.Toolkit.Skia.WinUI.csproj b/src/Uno.Toolkit.Skia.WinUI/Uno.Toolkit.Skia.WinUI.csproj
new file mode 100644
index 000000000..ff18c3a0c
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Uno.Toolkit.Skia.WinUI.csproj
@@ -0,0 +1,49 @@
+
+
+ $(TargetFrameworkOverride)
+ netstandard2.0;xamarinios10;xamarinmac20;MonoAndroid12.0
+ $(TargetFrameworks);net6.0-ios;net6.0-macos;net6.0-android;net6.0-maccatalyst
+ $(TargetFrameworks);net6.0-windows10.0.18362
+
+ true
+ Uno.Toolkit.Skia.WinUI
+ Uno.Toolkit.Skia.WinUI
+ Uno.Toolkit.Skia.WinUI
+ $(DefineConstants);WINDOWS
+ $(DefineConstants);IS_WINUI
+ WinUI
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Uno.Toolkit.UI/AssemblyInfo.cs b/src/Uno.Toolkit.UI/AssemblyInfo.cs
index 347e622f4..9dd1e37a2 100644
--- a/src/Uno.Toolkit.UI/AssemblyInfo.cs
+++ b/src/Uno.Toolkit.UI/AssemblyInfo.cs
@@ -1,4 +1,5 @@
-using System.Reflection;
+using System.ComponentModel;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -26,3 +27,9 @@
[assembly: InternalsVisibleTo("Uno.Toolkit.UI.Material")]
[assembly: InternalsVisibleTo("Uno.Toolkit.UI.Cupertino")]
[assembly: InternalsVisibleTo("Uno.Toolkit.UI")]
+
+#if IS_WINUI
+[assembly: InternalsVisibleTo("Uno.Toolkit.Skia.WinUI")]
+#else
+[assembly: InternalsVisibleTo("Uno.Toolkit.Skia.UI")]
+#endif
diff --git a/src/Uno.Toolkit.UI/MetaAttributes.cs b/src/Uno.Toolkit.UI/MetaAttributes.cs
new file mode 100644
index 000000000..dadcf2ac1
--- /dev/null
+++ b/src/Uno.Toolkit.UI/MetaAttributes.cs
@@ -0,0 +1,11 @@
+namespace System.Runtime.CompilerServices
+{
+#if !NET5_0 && !NET6_0_OR_GREATER
+ ///
+ /// Reserved to be used by the compiler for tracking metadata. This class should not be used by developers in source code.
+ ///
+ internal static class IsExternalInit
+ {
+ }
+#endif
+}
diff --git a/src/Uno.Toolkit.UI/Uno.Toolkit.UI.csproj b/src/Uno.Toolkit.UI/Uno.Toolkit.UI.csproj
index bdd413a48..94b0d7120 100644
--- a/src/Uno.Toolkit.UI/Uno.Toolkit.UI.csproj
+++ b/src/Uno.Toolkit.UI/Uno.Toolkit.UI.csproj
@@ -51,5 +51,9 @@
+
+
+
+
diff --git a/src/Uno.Toolkit.UI/Uno.Toolkit.WinUI.csproj b/src/Uno.Toolkit.UI/Uno.Toolkit.WinUI.csproj
index 4a008a990..2d3580366 100644
--- a/src/Uno.Toolkit.UI/Uno.Toolkit.WinUI.csproj
+++ b/src/Uno.Toolkit.UI/Uno.Toolkit.WinUI.csproj
@@ -1,4 +1,4 @@
-
+