diff --git a/build/workflow/scripts/wasm-uitest-run.sh b/build/workflow/scripts/wasm-uitest-run.sh
index 12707bfab..9dddf8922 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,7 +27,7 @@ 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
diff --git a/build/workflow/stage-uitests-android.yml b/build/workflow/stage-uitests-android.yml
index f79c18f7e..fe6e02ae6 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 6ce71ace2..9bbea03ac 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 01f3090aa..1b387410d 100644
--- a/build/workflow/stage-uitests-wasm.yml
+++ b/build/workflow/stage-uitests-wasm.yml
@@ -104,7 +104,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 72063c251..12d5a24f9 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: '7.0.102'
- UnoCheck_Version: '1.11.0-dev.2'
- UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/146b0b4b23d866bef455494a12ad7ffd2f6f2d20/manifests/uno.ui.manifest.json'
+ DotNetVersion: '7.0.302'
+ 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 dd50d9eda..b010ccdea 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: '7.0.102'
- UnoCheck_Version: '1.11.0-dev.2'
- UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/146b0b4b23d866bef455494a12ad7ffd2f6f2d20/manifests/uno.ui.manifest.json'
+ DotNetVersion: '7.0.302'
+ 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/assets/shadows-neumorphism.png b/doc/assets/shadows-neumorphism.png
new file mode 100644
index 000000000..47a73f28d
Binary files /dev/null and b/doc/assets/shadows-neumorphism.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/doc/controls/ShadowContainer.md b/doc/controls/ShadowContainer.md
new file mode 100644
index 000000000..d47447e87
--- /dev/null
+++ b/doc/controls/ShadowContainer.md
@@ -0,0 +1,288 @@
+---
+uid: Toolkit.Controls.ShadowContainer
+---
+
+## Summary
+The `ShadowContainer` provides the possibility to add many-colored shadows to its content.
+
+## Remarks
+For now, the control simply adapts its corner radius to the content's corner radius. More complicated shapes, such as text or pictures with alpha, are not supported.
+
+### XAML
+```xml
+xmlns:utu="using:Uno.Toolkit.UI"
+...
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Inheritance
+Object → DependencyObject → UIElement → FrameworkElement → Control → ContentControl > ShadowContainer
+
+### Properties
+| Property | Type | Description |
+| -------- | ---------------- | ----------- |
+Shadows | ShadowCollection | 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 throughout your app. The `ShadowCollection` implements `INotifyCollectionChanged`.
+
+## Shadow
+
+Dependency object representing a single shadow.
+
+### Shadow Properties
+| Property | Type | Description |
+| -------- | ---- | ----------- |
+IsInner | bool | True if this shadow is an inner shadow (similar to `inset` of `box-shadow` in CSS).
+OffsetX | double | The X offset of the shadow.
+OffsetY | double | The Y offset of the shadow.
+Color | Color | The color of the shadow. It will be multiplied by the `Opacity` property before rendering.
+Opacity | double | The opacity of the shadow.
+BlurRadius | double | The radius of the blur that will be applied to the shadow **[0..100]**.
+Spread | double | The spread will inflate or deflate (if negative) the control shadow size **before** applying the blur.
+
+## Usage
+
+```xml
+xmlns:utu="using:Uno.Toolkit.UI"
+...
+
+
+ #7a67f8
+ #f85977
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+![2 colored shadows with 2 buttons with single shadows](../assets/shadows-colors.png)
+
+### Neumorphism
+
+[Following neumorphism rules](https://neumorphism.io), choose one background color, 2 shadow colors, and create a cool neumorphism style.
+In order to achieve neumorphic depth effects (instead of having a raised element, it will feel as if it was hollow or bulging), set the `IsInner` property of a shadow to `True`. The shadow will then be displayed *inside* the element instead of behind.
+This is equivalent to the `inset` property of the CSS `box-shadow`.
+
+```xml
+xmlns:utu="using:Uno.Toolkit.UI"
+...
+
+
+ #7a67f8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+![neumorphism built with 2 shadows](../assets/shadows-neumorphism.png)
diff --git a/samples/Directory.Packages.props b/samples/Directory.Packages.props
index c6129650c..48bd526fe 100644
--- a/samples/Directory.Packages.props
+++ b/samples/Directory.Packages.props
@@ -17,11 +17,11 @@
-
-
-
-
-
+
+
+
+
+
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/Directory.Build.targets b/samples/Uno.Toolkit.Samples/Directory.Build.targets
new file mode 100644
index 000000000..7daa20833
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
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/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.Mobile/Uno.Toolkit.WinUI.Samples.Mobile.csproj b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Mobile/Uno.Toolkit.WinUI.Samples.Mobile.csproj
index c2a078435..81b60b0b1 100644
--- a/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Mobile/Uno.Toolkit.WinUI.Samples.Mobile.csproj
+++ b/samples/Uno.Toolkit.WinUI.Samples/Uno.Toolkit.WinUI.Samples.Mobile/Uno.Toolkit.WinUI.Samples.Mobile.csproj
@@ -94,6 +94,7 @@
+
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 a17e70ae3..825562ce4 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 024d8faa7..a66d6f1bc 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 9e5fb5e17..d40b5aafb 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,6 +27,7 @@
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 6d2b72b7a..fb790e16b 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..a0b327571
--- /dev/null
+++ b/src/Uno.Toolkit.RuntimeTests/Tests/ShadowContainerTests.cs
@@ -0,0 +1,161 @@
+#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]
+ 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..836372281 100644
--- a/src/Uno.Toolkit.RuntimeTests/Uno.Toolkit.RuntimeTests.WinUI.csproj
+++ b/src/Uno.Toolkit.RuntimeTests/Uno.Toolkit.RuntimeTests.WinUI.csproj
@@ -7,5 +7,6 @@
+
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..02894789a
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs
@@ -0,0 +1,219 @@
+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()
+ {
+ DefaultStyleKey = typeof(ShadowContainer);
+
+ _cornerRadius = new CornerRadius(0);
+
+ Loaded += ShadowContainerLoaded;
+ Unloaded += ShadowContainerUnloaded;
+ }
+
+ 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..878ed87ca
--- /dev/null
+++ b/src/Uno.Toolkit.Skia.WinUI/Uno.Toolkit.Skia.WinUI.csproj
@@ -0,0 +1,47 @@
+
+
+ $(TargetFrameworkOverride)
+ net7.0
+ $(TargetFrameworks);net7.0-ios;net7.0-macos;net7.0-android;net7.0-maccatalyst
+ $(TargetFrameworks);net7.0-windows10.0.19041
+
+ 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 eefd973fe..ade8c9985 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;
@@ -22,3 +23,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 4d6c1f344..8205850d3 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 38fb8559a..1d24772e5 100644
--- a/src/Uno.Toolkit.UI/Uno.Toolkit.WinUI.csproj
+++ b/src/Uno.Toolkit.UI/Uno.Toolkit.WinUI.csproj
@@ -1,4 +1,4 @@
-
+