diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index ca210300ee..d812818ed8 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,7 +2,7 @@ xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.MainView" - Background="Transparent" + Background="Black" Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> @@ -14,6 +14,7 @@ + @@ -80,7 +81,7 @@ Simple - Light Simple - Dark - + None Transparent Blur diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml new file mode 100644 index 0000000000..96cfcc5288 --- /dev/null +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs new file mode 100644 index 0000000000..44e7c4b92b --- /dev/null +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class AcrylicPage : UserControl + { + public AcrylicPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs new file mode 100644 index 0000000000..e1f1c4029e --- /dev/null +++ b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls +{ + /// + /// Defines compensation levels for the platform depending on the transparency level. + /// It controls the base opacity level of the 'tracing paper' layer that compensates + /// for low blur radius. + /// + public struct AcrylicPlatformCompensationLevels + { + public AcrylicPlatformCompensationLevels(double transparent, double blurred, double acrylic) + { + TransparentLevel = transparent; + BlurLevel = blurred; + AcrylicBlurLevel = acrylic; + } + + public double TransparentLevel { get; } + + public double BlurLevel { get; } + + public double AcrylicBlurLevel { get; } + } +} diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 48fc8d182f..f8bd2878d9 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -52,6 +52,9 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action TransparencyLevelChanged { get; set; } + /// + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); + public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; public virtual Point PointToClient(PixelPoint point) => point.ToPoint(1); diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs new file mode 100644 index 0000000000..0a01767a07 --- /dev/null +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -0,0 +1,117 @@ +using Avalonia.Controls.Utils; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; +using System; + +namespace Avalonia.Controls +{ + public class ExperimentalAcrylicBorder : Decorator + { + public static readonly StyledProperty CornerRadiusProperty = + Border.CornerRadiusProperty.AddOwner(); + + public static readonly StyledProperty MaterialProperty = + AvaloniaProperty.Register(nameof(Material)); + + private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); + + private IDisposable _subscription; + + static ExperimentalAcrylicBorder() + { + AffectsRender( + MaterialProperty, + CornerRadiusProperty); + } + + + /// + /// Gets or sets the radius of the border rounded corners. + /// + public CornerRadius CornerRadius + { + get { return GetValue(CornerRadiusProperty); } + set { SetValue(CornerRadiusProperty, value); } + } + + public ExperimentalAcrylicMaterial Material + { + get => GetValue(MaterialProperty); + set => SetValue(MaterialProperty, value); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + var tl = (e.Root as TopLevel); + + _subscription = tl.GetObservable(TopLevel.ActualTransparencyLevelProperty) + .Subscribe(x => + { + switch (x) + { + case WindowTransparencyLevel.Transparent: + case WindowTransparencyLevel.None: + Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.TransparentLevel; + break; + + case WindowTransparencyLevel.Blur: + Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.BlurLevel; + break; + + case WindowTransparencyLevel.AcrylicBlur: + Material.PlatformTransparencyCompensationLevel = tl.PlatformImpl.AcrylicCompensationLevels.AcrylicBlurLevel; + break; + } + }); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _subscription?.Dispose(); + } + + public override void Render(DrawingContext context) + { + if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) + { + var cornerRadius = CornerRadius; + + idc.DrawRectangle( + Material, + new RoundedRect( + new Rect(Bounds.Size), + cornerRadius.TopLeft, cornerRadius.TopRight, + cornerRadius.BottomRight, cornerRadius.BottomLeft)); + } + else + { + _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default); + } + } + + /// + /// Measures the control. + /// + /// The available size. + /// The desired size of the control. + protected override Size MeasureOverride(Size availableSize) + { + return LayoutHelper.MeasureChild(Child, availableSize, Padding); + } + + /// + /// Arranges the control's child. + /// + /// The size allocated to the control. + /// The space taken. + protected override Size ArrangeOverride(Size finalSize) + { + return LayoutHelper.ArrangeChild(Child, finalSize, Padding); + } + } +} diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 6472fba48e..0d77cbf802 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -127,5 +127,10 @@ namespace Avalonia.Platform /// Gets the current of the TopLevel. /// WindowTransparencyLevel TransparencyLevel { get; } + + /// + /// Gets the for the platform. + /// + AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 84c52d6fbf..168cdbc03f 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -174,6 +174,8 @@ namespace Avalonia.DesignerSupport.Remote public bool IsClientAreaExtendedToDecorations { get; } public bool NeedsManagedDecorations => false; + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f19f9e97aa..f916e95d7c 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -439,6 +439,8 @@ namespace Avalonia.Native public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0); + public IPlatformHandle Handle { get; private set; } } } diff --git a/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs new file mode 100644 index 0000000000..34cede4018 --- /dev/null +++ b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Media +{ + /// + /// Background Sources for Acrylic. + /// + public enum AcrylicBackgroundSource + { + /// + /// The acrylic has no background. + /// + None, + + /// + /// Cuts through all render layers to reveal the window background. + /// This means if your window is transparent or blurred it + /// will be blended with the material. + /// + Digger + } +} diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..6b699acd2e --- /dev/null +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -0,0 +1,345 @@ +using System; + +namespace Avalonia.Media +{ + public class ExperimentalAcrylicMaterial : AvaloniaObject, IMutableExperimentalAcrylicMaterial + { + private Color _effectiveTintColor; + private Color _effectiveLuminosityColor; + + static ExperimentalAcrylicMaterial() + { + AffectsRender( + TintColorProperty, + BackgroundSourceProperty, + TintOpacityProperty, + MaterialOpacityProperty, + PlatformTransparencyCompensationLevelProperty); + + TintColorProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + TintOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + MaterialOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + PlatformTransparencyCompensationLevelProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty TintColorProperty = + AvaloniaProperty.Register(nameof(TintColor)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundSourceProperty = + AvaloniaProperty.Register(nameof(BackgroundSource)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TintOpacityProperty = + AvaloniaProperty.Register(nameof(TintOpacity), 0.8); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaterialOpacityProperty = + AvaloniaProperty.Register(nameof(MaterialOpacity), 0.5); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlatformTransparencyCompensationLevelProperty = + AvaloniaProperty.Register(nameof(PlatformTransparencyCompensationLevel), 0.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FallbackColorProperty = + AvaloniaProperty.Register(nameof(FallbackColor)); + + /// + public event EventHandler Invalidated; + + /// + /// Gets or Sets the BackgroundSource . + /// + public AcrylicBackgroundSource BackgroundSource + { + get => GetValue(BackgroundSourceProperty); + set => SetValue(BackgroundSourceProperty, value); + } + + /// + /// Gets or Sets the TintColor. + /// + public Color TintColor + { + get => GetValue(TintColorProperty); + set => SetValue(TintColorProperty, value); + } + + /// + /// Gets or Sets the Tint Opacity. + /// + public double TintOpacity + { + get => GetValue(TintOpacityProperty); + set => SetValue(TintOpacityProperty, value); + } + + /// + /// Gets or Sets the Fallback Color. + /// This is used on rendering plaforms that dont support acrylic. + /// + public Color FallbackColor + { + get => GetValue(FallbackColorProperty); + set => SetValue(FallbackColorProperty, value); + } + + /// + /// Gets or Sets the MaterialOpacity. + /// This makes the material more or less opaque. + /// + public double MaterialOpacity + { + get => GetValue(MaterialOpacityProperty); + set => SetValue(MaterialOpacityProperty, value); + } + + /// + /// Gets or Sets the PlatformTransparencyCompensationLevel. + /// This value defines the minimum that can be used. + /// It means material opacity is re-scaled from this value to 1. + /// + public double PlatformTransparencyCompensationLevel + { + get => GetValue(PlatformTransparencyCompensationLevelProperty); + set => SetValue(PlatformTransparencyCompensationLevelProperty, value); + } + + Color IExperimentalAcrylicMaterial.MaterialColor => _effectiveLuminosityColor; + + Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor; + + private struct HsvColor + { + public float Hue { get; set; } + public float Saturation { get; set; } + public float Value { get; set; } + } + + private static HsvColor RgbToHsv(Color color) + { + var r = color.R / 255.0f; + var g = color.G / 255.0f; + var b = color.B / 255.0f; + var max = Math.Max(r, Math.Max(g, b)); + var min = Math.Min(r, Math.Min(g, b)); + + float h, s, v; + h = v = max; + + var d = max - min; + s = max == 0 ? 0 : d / max; + + if (max == min) + { + h = 0; // achromatic + } + else + { + if (max == r) + { + h = (g - b) / d + (g < b ? 6 : 0); + } + else if (max == g) + { + h = (b - r) / d + 2; + } + else if (max == b) + { + h = (r - g) / d + 4; + } + + h /= 6; + } + + return new HsvColor { Hue = h, Saturation = s, Value = v }; + } + + private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity) + { + // Update tintColor's alpha with the combined opacity value + double tintOpacityModifier = GetTintOpacityModifier(tintColor); + + return new Color((byte)(255 * ((255.0 / tintColor.A) * tintOpacity) * tintOpacityModifier), tintColor.R, tintColor.G, tintColor.B); + } + + private static double GetTintOpacityModifier(Color tintColor) + { + // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by + // compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure + // white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity). The intensity of + // the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out + // linearly as saturation increases from zero. + + const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. + + const double whiteMaxOpacity = 0.2; // 100% luminosity + const double midPointMaxOpacity = 0.45; // 50% luminosity + const double blackMaxOpacity = 0.45; // 0% luminosity + + var hsv = RgbToHsv(tintColor); + + double opacityModifier = midPointMaxOpacity; + + if (hsv.Value != midPoint) + { + // Determine maximum suppression amount + double lowestMaxOpacity = midPointMaxOpacity; + double maxDeviation = midPoint; + + if (hsv.Value > midPoint) + { + lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV) + maxDeviation = 1 - maxDeviation; + } + else if (hsv.Value < midPoint) + { + lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV) + } + + double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity; + + // Determine normalized deviation from the midpoint + double deviation = Math.Abs(hsv.Value - midPoint); + double normalizedDeviation = deviation / maxDeviation; + + // If we have saturation, reduce opacity suppression to allow that color to come through more + if (hsv.Saturation > 0) + { + // Dampen opacity suppression based on how much saturation there is + maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); + } + + double opacitySuppression = maxOpacitySuppression * normalizedDeviation; + + opacityModifier = midPointMaxOpacity - opacitySuppression; + } + + return opacityModifier; + } + + private Color GetEffectiveLuminosityColor() + { + double? luminosityOpacity = MaterialOpacity; + + return GetLuminosityColor(luminosityOpacity); + } + + private static byte Trim(double value) + { + value = Math.Min(Math.Floor(value * 256), 255); + + if (value < 0) + { + return 0; + } + else if (value > 255) + { + return 255; + } + + return (byte)value; + } + + private static float RGBMax(Color color) + { + if (color.R > color.G) + return (color.R > color.B) ? color.R : color.B; + else + return (color.G > color.B) ? color.G : color.B; + } + + private static float RGBMin(Color color) + { + if (color.R < color.G) + return (color.R < color.B) ? color.R : color.B; + else + return (color.G < color.B) ? color.G : color.B; + } + + // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity + private Color GetLuminosityColor(double? luminosityOpacity) + { + // Calculate the HSL lightness value of the color. + var max = (float)RGBMax(TintColor) / 255.0f; + var min = (float)RGBMin(TintColor) / 255.0f; + + var lightness = (max + min) / 2.0; + + lightness = 1 - ((1 - lightness) * luminosityOpacity.Value); + + lightness = 0.13 + (lightness * 0.74); + + var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); + + var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; + return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + (luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + } + + /// + /// Marks a property as affecting the brush's visual representation. + /// + /// The properties. + /// + /// After a call to this method in a brush's static constructor, any change to the + /// property will cause the event to be raised on the brush. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : ExperimentalAcrylicMaterial + { + static void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); + } + + foreach (var property in properties) + { + property.Changed.Subscribe(e => Invalidate(e)); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + public IExperimentalAcrylicMaterial ToImmutable() + { + return new ImmutableExperimentalAcrylicMaterial(this); + } + } +} diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs index 7756e94598..15b7681be4 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -13,4 +13,4 @@ namespace Avalonia.Media /// double Opacity { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..e71584258a --- /dev/null +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs @@ -0,0 +1,33 @@ +namespace Avalonia.Media +{ + /// + /// Experimental Interface for producing Acrylic-like materials. + /// + public interface IExperimentalAcrylicMaterial + { + /// + /// Gets the of the material. + /// + AcrylicBackgroundSource BackgroundSource { get; } + + /// + /// Gets the TintColor of the material. + /// + Color TintColor { get; } + + /// + /// Gets the TintOpacity of the material. + /// + double TintOpacity { get; } + + /// + /// Gets the effective material color. + /// + Color MaterialColor { get; } + + /// + /// Gets the fallback color. + /// + Color FallbackColor { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..fcfe4631a6 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Media +{ + /// + /// Represents a mutable brush which can return an immutable clone of itself. + /// + public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender + { + /// + /// Creates an immutable clone of the brush. + /// + /// The immutable clone. + IExperimentalAcrylicMaterial ToImmutable(); + } +} diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..f46d76cf3f --- /dev/null +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,73 @@ +using System; + +namespace Avalonia.Media +{ + public readonly struct ImmutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IEquatable + { + public ImmutableExperimentalAcrylicMaterial(IExperimentalAcrylicMaterial brush) + { + BackgroundSource = brush.BackgroundSource; + TintColor = brush.TintColor; + TintOpacity = brush.TintOpacity; + FallbackColor = brush.FallbackColor; + MaterialColor = brush.MaterialColor; + } + + public AcrylicBackgroundSource BackgroundSource { get; } + + public Color TintColor { get; } + + public Color MaterialColor { get; } + + public double TintOpacity { get; } + + public Color FallbackColor { get; } + + public bool Equals(ImmutableExperimentalAcrylicMaterial other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return + TintColor == other.TintColor && + TintOpacity == other.TintOpacity && + BackgroundSource == other.BackgroundSource && + FallbackColor == other.FallbackColor && MaterialColor == other.MaterialColor; + + } + + public override bool Equals(object obj) + { + return obj is ImmutableExperimentalAcrylicMaterial other && Equals(other); + } + + public Color GetEffectiveTintColor() + { + return TintColor; + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = (hash * 23) + TintColor.GetHashCode(); + hash = (hash * 23) + TintOpacity.GetHashCode(); + hash = (hash * 23) + BackgroundSource.GetHashCode(); + hash = (hash * 23) + FallbackColor.GetHashCode(); + hash = (hash * 23) + MaterialColor.GetHashCode(); + + return hash; + } + } + + public static bool operator ==(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/MaterialExtensions.cs b/src/Avalonia.Visuals/Media/MaterialExtensions.cs new file mode 100644 index 0000000000..c0b445c357 --- /dev/null +++ b/src/Avalonia.Visuals/Media/MaterialExtensions.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Media +{ + public static class MaterialExtensions + { + /// + /// Converts a brush to an immutable brush. + /// + /// The brush. + /// + /// The result of calling if the brush is mutable, + /// otherwise . + /// + public static IExperimentalAcrylicMaterial ToImmutable(this IExperimentalAcrylicMaterial material) + { + Contract.Requires(material != null); + + return (material as IMutableExperimentalAcrylicMaterial)?.ToImmutable() ?? material; + } + } +} diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs new file mode 100644 index 0000000000..a3cf0298dd --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs @@ -0,0 +1,9 @@ +using Avalonia.Media; + +namespace Avalonia.Platform +{ + public interface IDrawingContextWithAcrylicLikeSupport + { + void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index dfb21a0289..4a364998fd 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A drawing context which builds a scene graph. /// - internal class DeferredDrawingContextImpl : IDrawingContextImpl + internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private readonly ISceneBuilder _sceneBuilder; private VisualNode _node; @@ -164,6 +164,21 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationindex; + } + } + public void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs new file mode 100644 index 0000000000..336d11e3fd --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents a rectangle draw. + /// + internal class ExperimentalAcrylicNode : DrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The rectangle to draw. + /// The box shadow parameters + /// Child scenes for drawing visual brushes. + public ExperimentalAcrylicNode( + Matrix transform, + IExperimentalAcrylicMaterial material, + RoundedRect rect) + : base(rect.Rect, transform) + { + Transform = transform; + Material = material?.ToImmutable(); + Rect = rect; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + public IExperimentalAcrylicMaterial Material { get; } + + /// + /// Gets the rectangle to draw. + /// + public RoundedRect Rect { get; } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The fill of the other draw operation. + /// The rectangle of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + public bool Equals(Matrix transform, IExperimentalAcrylicMaterial material, RoundedRect rect) + { + return transform == Transform && + Equals(material, Material) && + rect.Equals(Rect); + } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + + if(context is IDrawingContextWithAcrylicLikeSupport idc) + { + idc.DrawRectangle(Material, Rect); + } + else + { + context.DrawRectangle(new ImmutableSolidColorBrush(Material.FallbackColor), null, Rect); + } + } + + /// + public override bool HitTest(Point p) + { + // TODO: This doesn't respect CornerRadius yet. + if (Transform.HasInverse) + { + p *= Transform.Invert(); + + if (Material != null) + { + var rect = Rect.Rect; + return rect.Contains(p); + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 5059a6d042..ec1a7753b1 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The box shadow parameters + /// The box shadow parameters /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 56ac7c833e..2a13999e8d 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1128,6 +1128,8 @@ namespace Avalonia.X11 public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel; + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); + public bool NeedsManagedDecorations => false; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 2153344363..b8ae2eb4d8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -82,5 +82,7 @@ namespace Avalonia.LinuxFramebuffer public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png new file mode 100644 index 0000000000..41de173d90 Binary files /dev/null and b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png differ diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 68da513528..d5ad70b944 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -7,6 +7,9 @@ true true + + + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ae756f4eab..a510763f64 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Skia /// /// Skia based drawing context. /// - internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl + internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDisposable[] _disposables; private readonly Vector _dpi; @@ -34,6 +34,7 @@ namespace Avalonia.Skia private readonly SKPaint _strokePaint = new SKPaint(); private readonly SKPaint _fillPaint = new SKPaint(); private readonly SKPaint _boxShadowPaint = new SKPaint(); + private static SKShader s_acrylicNoiseShader; /// /// Context create info. @@ -227,6 +228,41 @@ namespace Avalonia.Skia return bounds; } + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) + return; + + var rc = rect.Rect.ToSKRect(); + var isRounded = rect.IsRounded; + var needRoundRect = rect.IsRounded; + using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + + if (needRoundRect) + skRoundRect.SetRectRadii(rc, + new[] + { + rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + }); + + if (material != null) + { + using (var paint = CreateAcrylicPaint(_fillPaint, material)) + { + if (isRounded) + { + Canvas.DrawRoundRect(skRoundRect, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + + } + } + } /// public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) @@ -659,6 +695,86 @@ namespace Avalonia.Skia } } + static SKColorFilter CreateAlphaColorFilter(double opacity) + { + if (opacity > 1) + opacity = 1; + var c = new byte[256]; + var a = new byte[256]; + for (var i = 0; i < 256; i++) + { + c[i] = (byte)i; + a[i] = (byte)(i * opacity); + } + + return SKColorFilter.CreateTable(a, c, c, c); + } + + static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) + { + var ca = leftColor / 255d; + var aa = leftAlpha / 255d; + var cb = rightColor / 255d; + var ab = rightAlpha / 255d; + var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa)); + return (byte)(r * 255); + } + + static Color Blend(Color left, Color right) + { + var aa = left.A / 255d; + var ab = right.A / 255d; + return new Color( + (byte)((aa + ab * (1 - aa)) * 255), + Blend(left.R, left.A, right.R, right.A), + Blend(left.G, left.A, right.G, right.A), + Blend(left.B, left.A, right.B, right.A) + ); + } + + internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material, bool disposePaint = false) + { + var paintWrapper = new PaintWrapper(paint, disposePaint); + + paint.IsAntialias = true; + + double opacity = _currentOpacity; + + var tintOpacity = + material.BackgroundSource == AcrylicBackgroundSource.Digger ? + material.TintOpacity : 1; + + const double noiseOpcity = 0.0225; + + var tintColor = material.TintColor; + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); + + if (s_acrylicNoiseShader == null) + { + using (var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) + using (var bitmap = SKBitmap.Decode(stream)) + { + s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat) + .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); + } + } + + using (var backdrop = SKShader.CreateColor(new SKColor(material.MaterialColor.R, material.MaterialColor.G, material.MaterialColor.B, material.MaterialColor.A))) + using (var tintShader = SKShader.CreateColor(tint)) + using (var effectiveTint = SKShader.CreateCompose(backdrop, tintShader)) + using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) + { + paint.Shader = compose; + + if (material.BackgroundSource == AcrylicBackgroundSource.Digger) + { + paint.BlendMode = SKBlendMode.Src; + } + + return paintWrapper; + } + } + /// /// Creates paint wrapper for given brush. /// @@ -811,7 +927,7 @@ namespace Avalonia.Skia }; return new SurfaceRenderTarget(createInfo); - } + } /// /// Skia cached paint state. diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index f8366abb81..f5d83611bb 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -256,5 +256,7 @@ namespace Avalonia.Win32.Interop.Wpf public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f7e8a0a3e2..6e9cb81b11 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -277,7 +277,7 @@ namespace Avalonia.Win32 } accent.AccentFlags = 2; - accent.GradientColor = 0x00FFFFFF; + accent.GradientColor = 0x01000000; var accentPtr = Marshal.AllocHGlobal(accentStructSize); Marshal.StructureToPtr(accent, accentPtr, false); @@ -1071,6 +1071,9 @@ namespace Avalonia.Win32 /// public Thickness OffScreenMargin => _offScreenMargin; + /// + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + private struct SavedWindowInfo { public WindowStyles Style { get; set; }