Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ContentDialog elevation on WASM & Android, partially on iOS #7022

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/Uno.UI.Toolkit/ElevatedView.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Uno.UI.Helpers;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand Down Expand Up @@ -185,22 +186,23 @@ private void UpdateElevation()

if (Background == null)
{
this.SetElevationInternal(0, default);
ElevationHelper.SetElevation(this, 0, default);
}
else
{
#if __WASM__
this.SetElevationInternal(Elevation, ShadowColor);
ElevationHelper.SetElevation(this, Elevation, ShadowColor);
this.SetCornerRadius(CornerRadius);
#elif __IOS__ || __MACOS__
this.SetElevationInternal(Elevation, ShadowColor, _border.BoundsPath);
ElevationHelper.SetElevation(this, Elevation, ShadowColor, _border.BoundsPath);
#elif __ANDROID__
_invalidateShadow = true;
((ViewGroup)this).Invalidate();
// The elevation must be applied on the border, since
// it will get the right shape (with rounded corners)
ElevationHelper.SetElevation(_border, Elevation, ShadowColor);
#elif __SKIA__
this.SetElevationInternal(Elevation, ShadowColor);
ElevationHelper.SetElevation(this, Elevation, ShadowColor);
#elif (NETFX_CORE || NETCOREAPP) && !HAS_UNO
_border.SetElevationInternal(Elevation, ShadowColor, _shadowHost as DependencyObject, CornerRadius);
ElevationHelper.SetElevation(_border, Elevation, ShadowColor, _shadowHost as DependencyObject, CornerRadius);
#endif
}
}
Expand Down
95 changes: 95 additions & 0 deletions src/Uno.UI.Toolkit/Helpers/ElevationHelper.windows.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#if !HAS_UNO && !HAS_UNO_WINUI
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection;
using Windows.UI;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Hosting;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;
using Uno.Extensions;
using Uno.Logging;
using Uno.UI.Helpers;

namespace Uno.UI.Helpers
{
internal static class ElevationHelper
{
internal static void SetElevation(this DependencyObject element, double elevation, Color shadowColor, DependencyObject host = null, CornerRadius cornerRadius = default(CornerRadius))
{
if (element is UIElement uiElement)
{
var compositor = ElementCompositionPreview.GetElementVisual(uiElement).Compositor;
var spriteVisual = compositor.CreateSpriteVisual();

var newSize = new Vector2(0, 0);
if (uiElement is FrameworkElement contentFE)
{
newSize = new Vector2((float)contentFE.ActualWidth, (float)contentFE.ActualHeight);
}

if (!(host is Canvas uiHost) || newSize == default)
{
return;
}

spriteVisual.Size = newSize;
if (elevation > 0)
{
// Values for 1dp elevation according to https://material.io/guidelines/resources/shadows.html#shadows-illustrator
const float x = 0.25f;
const float y = 0.92f * 0.5f; // Looks more accurate than the recommended 0.92f.
const float blur = 0.5f;

var shadow = compositor.CreateDropShadow();
shadow.Offset = new Vector3((float)elevation * x, (float)elevation * y, -(float)elevation);
shadow.BlurRadius = (float)(blur * elevation);

shadow.Mask = uiElement switch
{
// GetAlphaMask is only available for shapes, images, and textblocks
Shape shape => shape.GetAlphaMask(),
Image image => image.GetAlphaMask(),
TextBlock tb => tb.GetAlphaMask(),
_ => shadow.Mask
};

if (!cornerRadius.Equals(default))
{
var averageRadius =
(cornerRadius.TopLeft +
cornerRadius.TopRight +
cornerRadius.BottomLeft +
cornerRadius.BottomRight) / 4f;

// Create a rectangle with similar corner radius (average for now)
var rect = new Rectangle()
{
Fill = new SolidColorBrush(Colors.White),
Width = newSize.X,
Height = newSize.Y,
RadiusX = averageRadius,
RadiusY = averageRadius
};

uiHost.Children.Add(rect); // The rect need to be in th VisualTree for .GetAlphaMask() to work

shadow.Mask = rect.GetAlphaMask();

uiHost.Children.Remove(rect); // No need anymore, we can discard it.
}

shadow.Color = shadowColor;
shadow.Opacity = shadowColor.A / 255f;
spriteVisual.Shadow = shadow;
}

ElementCompositionPreview.SetElementChildVisual(uiHost, spriteVisual);
}
}
}
}
#endif
160 changes: 2 additions & 158 deletions src/Uno.UI.Toolkit/UIElementExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,167 +69,11 @@ private static void OnElevationChanged(DependencyObject dependencyObject,
{
if (args.NewValue is double elevation)
{
SetElevationInternal(dependencyObject, elevation, ElevationColor);
Uno.UI.Helpers.ElevationHelper.SetElevation(dependencyObject, elevation, ElevationColor);
}
}

#if __IOS__ || __MACOS__
internal static void SetElevationInternal(this DependencyObject element, double elevation, Color shadowColor, CGPath path = null)
#elif (NETFX_CORE || NETCOREAPP) && !HAS_UNO
internal static void SetElevationInternal(this DependencyObject element, double elevation, Color shadowColor, DependencyObject host = null, CornerRadius cornerRadius = default(CornerRadius))
#else
internal static void SetElevationInternal(this DependencyObject element, double elevation, Color shadowColor)
#endif
{
#if __ANDROID__
if (element is Android.Views.View view)
{
AndroidX.Core.View.ViewCompat.SetElevation(view, (float)Uno.UI.ViewHelper.LogicalToPhysicalPixels(elevation));
if(Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.P)
{
view.SetOutlineAmbientShadowColor(shadowColor);
view.SetOutlineSpotShadowColor(shadowColor);
}
}
#elif __IOS__ || __MACOS__
#if __MACOS__
if (element is AppKit.NSView view)
#else
if (element is UIKit.UIView view)
#endif
{
if (elevation > 0)
{
const float x = 0.28f;
const float y = 0.92f * 0.5f;
const float blur = 0.18f;

#if __MACOS__
view.WantsLayer = true;
view.Shadow ??= new AppKit.NSShadow();
#endif
view.Layer.MasksToBounds = false;
view.Layer.ShadowOpacity = shadowColor.A / 255f;
#if __MACOS__
view.Layer.ShadowColor = AppKit.NSColor.FromRgb(shadowColor.R, shadowColor.G, shadowColor.B).CGColor;
#else
view.Layer.ShadowColor = UIKit.UIColor.FromRGB(shadowColor.R, shadowColor.G, shadowColor.B).CGColor;
#endif
view.Layer.ShadowRadius = (nfloat)(blur * elevation);
view.Layer.ShadowOffset = new CoreGraphics.CGSize(x * elevation, y * elevation);
view.Layer.ShadowPath = path;
}
else if(view.Layer != null)
{
view.Layer.ShadowOpacity = 0;
}
}
#elif __WASM__
if (element is UIElement uiElement)
{
if (elevation > 0)
{
const double x = 0.25d;
const double y = 0.92f * 0.5f;
const double blur = 0.3f;

var str = $"{(x * elevation).ToStringInvariant()}px {(y * elevation).ToStringInvariant()}px {(blur * elevation).ToStringInvariant()}px {shadowColor.ToCssString()}";
uiElement.SetStyle("box-shadow", str);
uiElement.SetCssClasses("noclip");
}
else
{
uiElement.ResetStyle("box-shadow");
uiElement.UnsetCssClasses("noclip");
}
}
#elif __SKIA__
if (element is UIElement uiElement)
{
var visual = uiElement.Visual;
const float x = 0.28f;
const float y = 0.92f * 0.5f;
const float blur = 0.18f;

var dx = (float)elevation * x;
var dy = (float)elevation * y;
var sigmaX = (float)(blur * elevation);
var sigmaY = (float)(blur * elevation);
var shadow = new ShadowState(dx, dy, sigmaX, sigmaY, shadowColor);
visual.ShadowState = shadow;
}
#elif (NETFX_CORE || NETCOREAPP) && !HAS_UNO
if (element is UIElement uiElement)
{
var compositor = ElementCompositionPreview.GetElementVisual(uiElement).Compositor;
var spriteVisual = compositor.CreateSpriteVisual();

var newSize = new Vector2(0, 0);
if (uiElement is FrameworkElement contentFE)
{
newSize = new Vector2((float)contentFE.ActualWidth, (float)contentFE.ActualHeight);
}

if (!(host is Canvas uiHost) || newSize == default)
{
return;
}

spriteVisual.Size = newSize;
if (elevation > 0)
{
const float x = 0.25f;
const float y = 0.92f * 0.5f;
const float blur = 0.5f;

var shadow = compositor.CreateDropShadow();
shadow.Offset = new Vector3((float)elevation*x, (float)elevation*y, -(float)elevation);
shadow.BlurRadius = (float)(blur * elevation);

shadow.Mask = uiElement switch
{
// GetAlphaMask is only available for shapes, images, and textblocks
Shape shape => shape.GetAlphaMask(),
Image image => image.GetAlphaMask(),
TextBlock tb => tb.GetAlphaMask(),
_ => shadow.Mask
};

if (!cornerRadius.Equals(default))
{
var averageRadius =
(cornerRadius.TopLeft +
cornerRadius.TopRight +
cornerRadius.BottomLeft +
cornerRadius.BottomRight) / 4f;

// Create a rectangle with similar corner radius (average for now)
var rect = new Rectangle()
{
Fill = new SolidColorBrush(Colors.White),
Width = newSize.X,
Height = newSize.Y,
RadiusX = averageRadius,
RadiusY = averageRadius
};

uiHost.Children.Add(rect); // The rect need to be in th VisualTree for .GetAlphaMask() to work

shadow.Mask = rect.GetAlphaMask();

uiHost.Children.Remove(rect); // No need anymore, we can discard it.
}

shadow.Color = shadowColor;
spriteVisual.Shadow = shadow;
}

ElementCompositionPreview.SetElementChildVisual(uiHost, spriteVisual);
}
#endif
}

#endregion
#endregion

internal static Thickness GetPadding(this UIElement uiElement)
{
Expand Down
19 changes: 19 additions & 0 deletions src/Uno.UI/Helpers/Elevation/ElevationHelper.Android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Uno.UI.Helpers;

internal static class ElevationHelper
{
internal static void SetElevation(DependencyObject element, double elevation, Color shadowColor)
{
if (element is not Android.Views.View view)
{
return;
}

AndroidX.Core.View.ViewCompat.SetElevation(view, (float)Uno.UI.ViewHelper.LogicalToPhysicalPixels(elevation));
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.P)
{
view.SetOutlineAmbientShadowColor(shadowColor);
view.SetOutlineSpotShadowColor(shadowColor);
}
}
}
45 changes: 45 additions & 0 deletions src/Uno.UI/Helpers/Elevation/ElevationHelper.iOSmacOS.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace Uno.UI.Helpers;

#if __IOS__
using _View = UIKit.UIView;
#else
using _View = AppKit.NSView;
#endif

internal static class ElevationHelper
{
internal static void SetElevation(DependencyObject element, double elevation, Color shadowColor, CGPath path = null)
{
if (element is not _View view)
{
return;
}

if (elevation > 0)
{
// Values for 1dp elevation according to https://material.io/guidelines/resources/shadows.html#shadows-illustrator
const float x = 0.25f;
const float y = 0.92f * 0.5f; // Looks more accurate than the recommended 0.92f.
const float blur = 0.5f;

#if __MACOS__
view.WantsLayer = true;
view.Shadow ??= new AppKit.NSShadow();
#endif
view.Layer.MasksToBounds = false;
view.Layer.ShadowOpacity = shadowColor.A / 255f;
#if __MACOS__
view.Layer.ShadowColor = AppKit.NSColor.FromRgb(shadowColor.R, shadowColor.G, shadowColor.B).CGColor;
#else
view.Layer.ShadowColor = UIKit.UIColor.FromRGB(shadowColor.R, shadowColor.G, shadowColor.B).CGColor;
#endif
view.Layer.ShadowRadius = (nfloat)(blur * elevation);
view.Layer.ShadowOffset = new CoreGraphics.CGSize(x * elevation, y * elevation);
view.Layer.ShadowPath = path;
}
else if (view.Layer != null)
{
view.Layer.ShadowOpacity = 0;
}
}
}
30 changes: 30 additions & 0 deletions src/Uno.UI/Helpers/Elevation/ElevationHelper.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Uno.UI.Composition.Composition;
using Windows.UI;
using Windows.UI.Xaml;

namespace Uno.UI.Helpers;

internal static class ElevationHelper
{
internal static void SetElevation(DependencyObject element, double elevation, Color shadowColor)
{
if (element is not UIElement uiElement)
{
return;
}

var visual = uiElement.Visual;

const float SHADOW_SIGMA_X_MODIFIER = 1f / 3.5f;
const float SHADOW_SIGMA_Y_MODIFIER = 1f / 3.5f;
float x = 0.3f;
float y = 0.92f * 0.5f;

var dx = (float)elevation * x;
var dy = (float)elevation * y;
var sigmaX = (float)elevation * SHADOW_SIGMA_X_MODIFIER;
var sigmaY = (float)elevation * SHADOW_SIGMA_Y_MODIFIER;
var shadow = new ShadowState(dx, dy, sigmaX, sigmaY, shadowColor);
visual.ShadowState = shadow;
}
}
Loading