Browse Source

Merge branch 'master' into x11-disable-input

pull/10919/head
Nikita Tsukanov 3 years ago
committed by GitHub
parent
commit
fd5dc0ddad
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 6
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  3. 22
      src/Avalonia.Base/Media/Effects/BlurEffect.cs
  4. 104
      src/Avalonia.Base/Media/Effects/DropShadowEffect.cs
  5. 93
      src/Avalonia.Base/Media/Effects/Effect.cs
  6. 131
      src/Avalonia.Base/Media/Effects/EffectAnimator.cs
  7. 18
      src/Avalonia.Base/Media/Effects/EffectConverter.cs
  8. 56
      src/Avalonia.Base/Media/Effects/EffectExtesions.cs
  9. 83
      src/Avalonia.Base/Media/Effects/EffectTransition.cs
  10. 29
      src/Avalonia.Base/Media/Effects/IBlurEffect.cs
  11. 84
      src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs
  12. 26
      src/Avalonia.Base/Media/Effects/IEffect.cs
  13. 92
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  14. 6
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  15. 9
      src/Avalonia.Base/Rect.cs
  16. 10
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  17. 56
      src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
  18. 15
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  19. 68
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  20. 28
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  21. 2
      src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
  22. 15
      src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
  23. 2
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
  24. 2
      src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
  25. 2
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  26. 5
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  27. 2
      src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
  28. 2
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  29. 2
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  30. 2
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  31. 18
      src/Avalonia.Base/StyledElement.cs
  32. 8
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  33. 20
      src/Avalonia.Base/Visual.cs
  34. 6
      src/Avalonia.Base/composition-schema.xml
  35. 28
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  36. 4
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  37. 29
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  38. 2
      src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
  39. 4
      src/Avalonia.Controls/Flyouts/Flyout.cs
  40. 76
      src/Avalonia.Controls/TextBlock.cs
  41. 2
      src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs
  42. 12
      src/Avalonia.Native/WindowImpl.cs
  43. 12
      src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml
  44. 12
      src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml
  45. 2
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  46. 5
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  47. 4
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  48. 59
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  49. 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  50. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  51. 46
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXAmlIlClassesTransformer.cs
  52. 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs
  53. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs
  54. 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs
  55. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs
  56. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  57. 50
      src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs
  58. 16
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  59. 37
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  60. 4
      src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs
  61. 73
      tests/Avalonia.Base.UnitTests/Media/EffectTests.cs
  62. 18
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
  63. 58
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  64. 16
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
  65. 60
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  66. 43
      tests/Avalonia.RenderTests/Media/EffectTests.cs
  67. 3
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  68. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  69. BIN
      tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png

40
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -308,6 +308,41 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Blur">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="blur(0)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="blur(10)"/>
</KeyFrame>
</Animation>
</Style.Animations>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
</Style>
<Style Selector="Border.DropShadow">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="drop-shadow(0 0 0)"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="Effect" Value="drop-shadow(5 5 0 Green)"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="Effect" Value="drop-shadow(5 5 5 Red)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="drop-shadow(20 -5 5 Blue)"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -332,6 +367,11 @@
<Border Classes="Test Rect8" Child="{x:Null}" />
<Border Classes="Test Rect9" Child="{x:Null}" />
<Border Classes="Test Rect10" Child="{x:Null}" />
<Border Classes="Test Blur" Background="#ffa0a0a0" BorderThickness="4" BorderBrush="Yellow" Padding="10"/>
<Border Classes="Test DropShadow" Background="Transparent" BorderThickness="4" BorderBrush="Yellow">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">Drop
Shadow</TextBlock>
</Border>
</WrapPanel>
</StackPanel>
</Grid>

6
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@ -81,7 +81,7 @@ namespace Avalonia.Android.Platform
var renderScaling = _topLevel.RenderScaling;
var inset = insets.GetInsets(
(DisplayEdgeToEdge ?
(_displayEdgeToEdge ?
WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
WindowInsetsCompat.Type.DisplayCutout() :
0) | WindowInsetsCompat.Type.Ime());
@ -91,8 +91,8 @@ namespace Avalonia.Android.Platform
return new Thickness(inset.Left / renderScaling,
inset.Top / renderScaling,
inset.Right / renderScaling,
(imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ?
imeInset.Bottom - navBarInset.Bottom :
(imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ?
imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) :
inset.Bottom) / renderScaling);
}

22
src/Avalonia.Base/Media/Effects/BlurEffect.cs

@ -0,0 +1,22 @@
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public class BlurEffect : Effect, IBlurEffect, IMutableEffect
{
public static readonly StyledProperty<double> RadiusProperty = AvaloniaProperty.Register<BlurEffect, double>(
nameof(Radius), 5);
public double Radius
{
get => GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
static BlurEffect()
{
AffectsRender<BlurEffect>(RadiusProperty);
}
public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius);
}

104
src/Avalonia.Base/Media/Effects/DropShadowEffect.cs

@ -0,0 +1,104 @@
// ReSharper disable once CheckNamespace
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public abstract class DropShadowEffectBase : Effect
{
public static readonly StyledProperty<double> BlurRadiusProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(BlurRadius), 5);
public double BlurRadius
{
get => GetValue(BlurRadiusProperty);
set => SetValue(BlurRadiusProperty, value);
}
public static readonly StyledProperty<Color> ColorProperty = AvaloniaProperty.Register<DropShadowEffectBase, Color>(
nameof(Color));
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(Opacity), 1);
public double Opacity
{
get => GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
static DropShadowEffectBase()
{
AffectsRender<DropShadowEffectBase>(BlurRadiusProperty, ColorProperty, OpacityProperty);
}
}
public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> OffsetXProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetX), 3.5355);
public double OffsetX
{
get => GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
}
public static readonly StyledProperty<double> OffsetYProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetY), 3.5355);
public double OffsetY
{
get => GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
}
static DropShadowEffect()
{
AffectsRender<DropShadowEffect>(OffsetXProperty, OffsetYProperty);
}
public IImmutableEffect ToImmutable()
{
return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}
}
/// <summary>
/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY
/// </summary>
public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> ShadowDepthProperty =
AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(ShadowDepth), 5);
public double ShadowDepth
{
get => GetValue(ShadowDepthProperty);
set => SetValue(ShadowDepthProperty, value);
}
public static readonly StyledProperty<double> DirectionProperty = AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(Direction), 315);
public double Direction
{
get => GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public double OffsetX => Math.Cos(Direction) * ShadowDepth;
public double OffsetY => Math.Sin(Direction) * ShadowDepth;
public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}

93
src/Avalonia.Base/Media/Effects/Effect.cs

@ -0,0 +1,93 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Animators;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Utilities;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public class Effect : Animatable, IAffectsRender
{
/// <summary>
/// Marks a property as affecting the brush's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Effect
{
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty));
foreach (var property in properties)
{
property.Changed.Subscribe(invalidateObserver);
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
/// <inheritdoc />
public event EventHandler? Invalidated;
static Exception ParseError(string s) => throw new ArgumentException("Unable to parse effect: " + s);
public static IEffect Parse(string s)
{
var span = s.AsSpan();
var r = new TokenParser(span);
if (r.TryConsume("blur"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var radius) || !r.TryConsume(')') || !r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableBlurEffect(radius);
}
if (r.TryConsume("drop-shadow"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var offsetX)
|| !r.TryParseDouble(out var offsetY))
throw ParseError(s);
double blurRadius = 0;
var color = Colors.Black;
if (!r.TryConsume(')'))
{
if (!r.TryParseDouble(out blurRadius) || blurRadius < 0)
throw ParseError(s);
if (!r.TryConsume(')'))
{
var endOfExpression = s.LastIndexOf(")", StringComparison.Ordinal);
if (endOfExpression == -1)
throw ParseError(s);
if (!new TokenParser(span.Slice(endOfExpression + 1)).IsEofWithWhitespace())
throw ParseError(s);
if (!Color.TryParse(span.Slice(r.Position, endOfExpression - r.Position).TrimEnd(), out color))
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
}
if (!r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
throw ParseError(s);
}
static Effect()
{
EffectAnimator.EnsureRegistered();
}
}

131
src/Avalonia.Base/Media/Effects/EffectAnimator.cs

@ -0,0 +1,131 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation.Animators;
public class EffectAnimator : Animator<IEffect?>
{
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
IObservable<bool> match, Action? onComplete)
{
if (TryCreateAnimator<BlurEffectAnimator, IBlurEffect>(out var animator)
|| TryCreateAnimator<DropShadowEffectAnimator, IDropShadowEffect>(out animator))
return animator.Apply(animation, control, clock, match, onComplete);
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
this,
"The animation's keyframe value types set is not supported.");
return base.Apply(animation, control, clock, match, onComplete);
}
private bool TryCreateAnimator<TAnimator, TInterface>([NotNullWhen(true)] out IAnimator? animator)
where TAnimator : EffectAnimatorBase<TInterface>, new() where TInterface : class, IEffect
{
TAnimator? createdAnimator = null;
foreach (var keyFrame in this)
{
if (keyFrame.Value is TInterface)
{
createdAnimator ??= new TAnimator()
{
Property = Property
};
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue,
keyFrame.KeySpline)
{
Value = keyFrame.Value
});
}
else
{
animator = null;
return false;
}
}
animator = createdAnimator;
return animator != null;
}
/// <summary>
/// Fallback implementation of <see cref="IEffect"/> animation.
/// </summary>
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) => progress >= 0.5 ? newValue : oldValue;
private static bool s_Registered;
public static void EnsureRegistered()
{
if(s_Registered)
return;
s_Registered = true;
Animation.RegisterAnimator<EffectAnimator>(prop =>
typeof(IEffect).IsAssignableFrom(prop.PropertyType));
}
}
public abstract class EffectAnimatorBase<T> : Animator<IEffect?> where T : class, IEffect?
{
public override IDisposable BindAnimation(Animatable control, IObservable<IEffect?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IEffect?>)Property, instance, BindingPriority.Animation);
}
protected abstract T Interpolate(double progress, T oldValue, T newValue);
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue)
{
var old = oldValue as T;
var n = newValue as T;
if (old == null || n == null)
return progress >= 0.5 ? newValue : oldValue;
return Interpolate(progress, old, n);
}
}
public class BlurEffectAnimator : EffectAnimatorBase<IBlurEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IBlurEffect Interpolate(double progress, IBlurEffect oldValue, IBlurEffect newValue)
{
return new ImmutableBlurEffect(
s_doubleAnimator.Interpolate(progress, oldValue.Radius, newValue.Radius));
}
}
public class DropShadowEffectAnimator : EffectAnimatorBase<IDropShadowEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IDropShadowEffect Interpolate(double progress, IDropShadowEffect oldValue,
IDropShadowEffect newValue)
{
var blur = s_doubleAnimator.Interpolate(progress, oldValue.BlurRadius, newValue.BlurRadius);
var color = ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color);
var opacity = s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity);
if (oldValue is IDirectionDropShadowEffect oldDirection && newValue is IDirectionDropShadowEffect newDirection)
{
return new ImmutableDropShadowDirectionEffect(
s_doubleAnimator.Interpolate(progress, oldDirection.Direction, newDirection.Direction),
s_doubleAnimator.Interpolate(progress, oldDirection.ShadowDepth, newDirection.ShadowDepth),
blur, color, opacity
);
}
return new ImmutableDropShadowEffect(
s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
blur, color, opacity
);
}
}

18
src/Avalonia.Base/Media/Effects/EffectConverter.cs

@ -0,0 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Media;
public class EffectConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
{
return value is string s ? Effect.Parse(s) : null;
}
}

56
src/Avalonia.Base/Media/Effects/EffectExtesions.cs

@ -0,0 +1,56 @@
using System;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public static class EffectExtensions
{
static double AdjustPaddingRadius(double radius)
{
if (radius <= 0)
return 0;
return Math.Ceiling(radius) + 1;
}
internal static Thickness GetEffectOutputPadding(this IEffect? effect)
{
if (effect == null)
return default;
if (effect is IBlurEffect blur)
return new Thickness(AdjustPaddingRadius(blur.Radius));
if (effect is IDropShadowEffect dropShadowEffect)
{
var radius = AdjustPaddingRadius(dropShadowEffect.BlurRadius);
var rc = new Rect(-radius, -radius,
radius * 2, radius * 2);
rc = rc.Translate(new(dropShadowEffect.OffsetX, dropShadowEffect.OffsetY));
return new Thickness(Math.Max(0, 0 - rc.X),
Math.Max(0, 0 - rc.Y), Math.Max(0, rc.Right), Math.Max(0, rc.Bottom));
}
throw new ArgumentException("Unknown effect type: " + effect.GetType());
}
/// <summary>
/// Converts a effect to an immutable effect.
/// </summary>
/// <param name="effect">The effect.</param>
/// <returns>
/// The result of calling <see cref="IMutableEffect.ToImmutable"/> if the effect is mutable,
/// otherwise <paramref name="effect"/>.
/// </returns>
public static IImmutableEffect ToImmutable(this IEffect effect)
{
_ = effect ?? throw new ArgumentNullException(nameof(effect));
return (effect as IMutableEffect)?.ToImmutable() ?? (IImmutableEffect)effect;
}
internal static bool EffectEquals(this IImmutableEffect? immutable, IEffect? right)
{
if (immutable == null && right == null)
return true;
if (immutable != null && right != null)
return immutable.Equals(right);
return false;
}
}

83
src/Avalonia.Base/Media/Effects/EffectTransition.cs

@ -0,0 +1,83 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation;
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IEffect"/> type.
/// </summary>
public class EffectTransition : Transition<IEffect?>
{
private static readonly BlurEffectAnimator s_blurEffectAnimator = new();
private static readonly DropShadowEffectAnimator s_dropShadowEffectAnimator = new();
private static readonly ImmutableBlurEffect s_DefaultBlur = new ImmutableBlurEffect(0);
private static readonly ImmutableDropShadowDirectionEffect s_DefaultDropShadow = new(0, 0, 0, default, 0);
bool TryWithAnimator<TAnimator, TInterface>(
IObservable<double> progress,
TAnimator animator,
IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable<IEffect?> observable)
where TAnimator : EffectAnimatorBase<TInterface> where TInterface : class, IEffect
{
observable = null;
TInterface? oldI = null, newI = null;
if (oldValue is TInterface oi)
{
oldI = oi;
if (newValue is TInterface ni)
newI = ni;
else if (newValue == null)
newI = defaultValue;
else
return false;
}
else if (newValue is TInterface nv)
{
oldI = defaultValue;
newI = nv;
}
else
return false;
observable = new AnimatorTransitionObservable<IEffect?, Animator<IEffect?>>(animator, progress, Easing, oldI, newI);
return true;
}
public override IObservable<IEffect?> DoTransition(IObservable<double> progress, IEffect? oldValue, IEffect? newValue)
{
if ((oldValue != null || newValue != null)
&& (
TryWithAnimator<BlurEffectAnimator, IBlurEffect>(progress, s_blurEffectAnimator,
oldValue, newValue, s_DefaultBlur, out var observable)
|| TryWithAnimator<DropShadowEffectAnimator, IDropShadowEffect>(progress, s_dropShadowEffectAnimator,
oldValue, newValue, s_DefaultDropShadow, out observable)
))
return observable;
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
}
private sealed class IncompatibleTransitionObservable : TransitionObservableBase<IEffect?>
{
private readonly IEffect? _from;
private readonly IEffect? _to;
public IncompatibleTransitionObservable(IObservable<double> progress, Easing easing, IEffect? from, IEffect? to) : base(progress, easing)
{
_from = from;
_to = to;
}
protected override IEffect? ProduceValue(double progress)
{
return progress >= 0.5 ? _to : _from;
}
}
}

29
src/Avalonia.Base/Media/Effects/IBlurEffect.cs

@ -0,0 +1,29 @@
// ReSharper disable once CheckNamespace
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IBlurEffect : IEffect
{
double Radius { get; }
}
public class ImmutableBlurEffect : IBlurEffect, IImmutableEffect
{
static ImmutableBlurEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableBlurEffect(double radius)
{
Radius = radius;
}
public double Radius { get; }
public bool Equals(IEffect? other) =>
// ReSharper disable once CompareOfFloatsByEqualityOperator
other is IBlurEffect blur && blur.Radius == Radius;
}

84
src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs

@ -0,0 +1,84 @@
// ReSharper disable once CheckNamespace
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IDropShadowEffect : IEffect
{
double OffsetX { get; }
double OffsetY { get; }
double BlurRadius { get; }
Color Color { get; }
double Opacity { get; }
}
internal interface IDirectionDropShadowEffect : IDropShadowEffect
{
double Direction { get; }
double ShadowDepth { get; }
}
public class ImmutableDropShadowEffect : IDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowEffect(double offsetX, double offsetY, double blurRadius, Color color, double opacity)
{
OffsetX = offsetX;
OffsetY = offsetY;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX { get; }
public double OffsetY { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}
public class ImmutableDropShadowDirectionEffect : IDirectionDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowDirectionEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowDirectionEffect(double direction, double shadowDepth, double blurRadius, Color color, double opacity)
{
Direction = direction;
ShadowDepth = shadowDepth;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX => Math.Cos(Direction) * ShadowDepth;
public double OffsetY => Math.Sin(Direction) * ShadowDepth;
public double Direction { get; }
public double ShadowDepth { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}

26
src/Avalonia.Base/Media/Effects/IEffect.cs

@ -0,0 +1,26 @@
// ReSharper disable once CheckNamespace
using System;
using System.ComponentModel;
namespace Avalonia.Media;
[TypeConverter(typeof(EffectConverter))]
public interface IEffect
{
}
public interface IMutableEffect : IEffect, IAffectsRender
{
/// <summary>
/// Creates an immutable clone of the effect.
/// </summary>
/// <returns>The immutable clone.</returns>
internal IImmutableEffect ToImmutable();
}
public interface IImmutableEffect : IEffect, IEquatable<IEffect>
{
}

92
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -57,21 +57,21 @@ namespace Avalonia.Media.TextFormatting
switch (paragraphProperties.TextWrapping)
{
case TextWrapping.NoWrap:
{
var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex,
textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
{
var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex,
textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine();
return textLine;
}
return textLine;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
}
{
return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
}
default:
throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping));
}
@ -568,9 +568,9 @@ namespace Avalonia.Media.TextFormatting
return false;
}
private static bool TryMeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth, out int measuredLength)
private static int MeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth)
{
measuredLength = 0;
var measuredLength = 0;
var currentWidth = 0.0;
for (var i = 0; i < textRuns.Count; ++i)
@ -583,25 +583,59 @@ namespace Avalonia.Media.TextFormatting
{
if (shapedTextCharacters.ShapedBuffer.Length > 0)
{
var firstCluster = shapedTextCharacters.ShapedBuffer[0].GlyphCluster;
var lastCluster = firstCluster;
var runLength = 0;
for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++)
{
var glyphInfo = shapedTextCharacters.ShapedBuffer[j];
var currentInfo = shapedTextCharacters.ShapedBuffer[j];
var clusterWidth = currentInfo.GlyphAdvance;
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
GlyphInfo nextInfo = default;
while (j + 1 < shapedTextCharacters.ShapedBuffer.Length)
{
measuredLength += Math.Max(0, lastCluster - firstCluster);
nextInfo = shapedTextCharacters.ShapedBuffer[j + 1];
if (currentInfo.GlyphCluster == nextInfo.GlyphCluster)
{
clusterWidth += nextInfo.GlyphAdvance;
j++;
continue;
}
break;
}
return measuredLength != 0;
var clusterLength = Math.Max(0, nextInfo.GlyphCluster - currentInfo.GlyphCluster);
if(clusterLength == 0)
{
clusterLength = currentRun.Length - runLength;
}
if(clusterLength == 0)
{
clusterLength = shapedTextCharacters.GlyphRun.Metrics.FirstCluster + currentRun.Length - currentInfo.GlyphCluster;
}
if (currentWidth + clusterWidth > paragraphWidth)
{
if (runLength == 0 && measuredLength == 0)
{
runLength = clusterLength;
}
return measuredLength + runLength;
}
lastCluster = glyphInfo.GlyphCluster;
currentWidth += glyphInfo.GlyphAdvance;
currentWidth += clusterWidth;
runLength += clusterLength;
}
measuredLength += currentRun.Length;
measuredLength += runLength;
}
break;
@ -611,7 +645,7 @@ namespace Avalonia.Media.TextFormatting
{
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
return measuredLength != 0;
return measuredLength;
}
measuredLength += currentRun.Length;
@ -628,7 +662,7 @@ namespace Avalonia.Media.TextFormatting
}
}
return measuredLength != 0;
return measuredLength;
}
/// <summary>
@ -675,9 +709,11 @@ namespace Avalonia.Media.TextFormatting
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
}
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
var measuredLength = MeasureLength(textRuns, paragraphWidth);
if(measuredLength == 0)
{
measuredLength = 1;
}
var currentLength = 0;
@ -798,6 +834,12 @@ namespace Avalonia.Media.TextFormatting
continue;
}
//We don't want to surpass the measuredLength with trailing whitespace when we are in a right to left setting.
if(currentPosition > measuredLength && resolvedFlowDirection == FlowDirection.RightToLeft)
{
break;
}
measuredLength = currentPosition;
break;

6
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -180,6 +180,12 @@ namespace Avalonia.Platform
object? GetFeature(Type t);
}
public interface IDrawingContextImplWithEffects
{
void PushEffect(IEffect effect);
void PopEffect();
}
public static class DrawingContextImplExtensions
{
/// <summary>

9
src/Avalonia.Base/Rect.cs

@ -526,6 +526,15 @@ namespace Avalonia
}
}
internal static Rect? Union(Rect? left, Rect? right)
{
if (left == null)
return right;
if (right == null)
return left;
return left.Value.Union(right.Value);
}
/// <summary>
/// Returns a new <see cref="Rect"/> with the specified X position.
/// </summary>

10
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -252,8 +252,14 @@ public class CompositingRenderer : IRendererWithCompositor
comp.Opacity = (float)visual.Opacity;
comp.ClipToBounds = visual.ClipToBounds;
comp.Clip = visual.Clip?.PlatformImpl;
comp.OpacityMask = visual.OpacityMask;
if (!Equals(comp.OpacityMask, visual.OpacityMask))
comp.OpacityMask = visual.OpacityMask?.ToImmutable();
if (!comp.Effect.EffectEquals(visual.Effect))
comp.Effect = visual.Effect?.ToImmutable();
var renderTransform = Matrix.Identity;
if (visual.HasMirrorTransform)

56
src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs

@ -29,6 +29,8 @@ namespace Avalonia.Rendering.Composition.Expressions
}
}
public bool NextIsWhitespace() => _s.Length > 0 && char.IsWhiteSpace(_s[0]);
static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z');
@ -238,6 +240,12 @@ namespace Avalonia.Rendering.Composition.Expressions
len = c + 1;
dotCount++;
}
else if (ch == '-')
{
if (len != 0)
return false;
len = c + 1;
}
else
break;
}
@ -254,7 +262,55 @@ namespace Avalonia.Rendering.Composition.Expressions
Advance(len);
return true;
}
public bool TryParseDouble(out double res)
{
res = 0;
SkipWhitespace();
if (_s.Length == 0)
return false;
var len = 0;
var dotCount = 0;
for (var c = 0; c < _s.Length; c++)
{
var ch = _s[c];
if (ch >= '0' && ch <= '9')
len = c + 1;
else if (ch == '.' && dotCount == 0)
{
len = c + 1;
dotCount++;
}
else if (ch == '-')
{
if (len != 0)
return false;
len = c + 1;
}
else
break;
}
var span = _s.Slice(0, len);
#if NETSTANDARD2_0
if (!double.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#else
if (!double.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#endif
Advance(len);
return true;
}
public bool IsEofWithWhitespace()
{
SkipWhitespace();
return Length == 0;
}
public override string ToString() => _s.ToString();
}

15
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -18,7 +18,8 @@ namespace Avalonia.Rendering.Composition.Server;
/// they have information about the full render transform (they are not)
/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation.
/// </summary>
internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
internal class CompositorDrawingContextProxy : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects
{
private IDrawingContextImpl _impl;
@ -155,4 +156,16 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic)
acrylic.DrawRectangle(material, rect);
}
public void PushEffect(IEffect effect)
{
if (_impl is IDrawingContextImplWithEffects effects)
effects.PushEffect(effect);
}
public void PopEffect()
{
if (_impl is IDrawingContextImplWithEffects effects)
effects.PopEffect();
}
}

68
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -1,4 +1,6 @@
using System;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Platform;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server
internal partial class ServerCompositionContainerVisual : ServerCompositionVisual
{
public ServerCompositionVisualCollection Children { get; private set; } = null!;
private Rect? _transformedContentBounds;
private IImmutableEffect? _oldEffect;
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
@ -24,18 +28,76 @@ namespace Avalonia.Rendering.Composition.Server
}
}
public override void Update(ServerCompositionTarget root)
public override UpdateResult Update(ServerCompositionTarget root)
{
base.Update(root);
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root);
foreach (var child in Children)
{
if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child);
else
child.Update(root);
{
var res = child.Update(root);
oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew;
combinedBounds = Rect.Union(combinedBounds, res.Bounds);
}
}
// If effect is changed, we need to clean both old and new bounds
var effectChanged = !Effect.EffectEquals(_oldEffect);
if (effectChanged)
oldInvalidated = newInvalidated = true;
// Expand invalidated bounds to the whole content area since we don't actually know what is being sampled
// We also ignore clip for now since we don't have means to reset it?
if (_oldEffect != null && oldInvalidated && _transformedContentBounds.HasValue)
AddEffectPaddedDirtyRect(_oldEffect, _transformedContentBounds.Value);
if (Effect != null && newInvalidated && combinedBounds.HasValue)
AddEffectPaddedDirtyRect(Effect, combinedBounds.Value);
_oldEffect = Effect;
_transformedContentBounds = combinedBounds;
IsDirtyComposition = false;
return new(_transformedContentBounds, oldInvalidated, newInvalidated);
}
void AddEffectPaddedDirtyRect(IImmutableEffect effect, Rect transformedBounds)
{
var padding = effect.GetEffectOutputPadding();
if (padding == default)
{
AddDirtyRect(transformedBounds);
return;
}
// We are in a weird position here: bounds are in global coordinates while padding gets applied in local ones
// Since we have optimizations to AVOID recomputing transformed bounds and since visuals with effects are relatively rare
// we instead apply the transformation matrix to rescale the bounds
// If we only have translation and scale, just scale the padding
if (CombinedTransformMatrix is
{
M12: 0, M13: 0, M14: 0,
M21: 0, M23: 0, M24: 0,
M31: 0, M32: 0, M34: 0,
M43: 0, M44: 1
})
padding = new Thickness(padding.Left * CombinedTransformMatrix.M11,
padding.Top * CombinedTransformMatrix.M22,
padding.Right * CombinedTransformMatrix.M11,
padding.Bottom * CombinedTransformMatrix.M22);
else
{
// Conservatively use the transformed rect size
var transformedPaddingRect = new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix);
padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height));
}
AddDirtyRect(transformedBounds.Inflate(padding));
}
partial void Initialize()

28
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -54,6 +54,9 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
if (Effect != null)
canvas.PushEffect(Effect);
if (Opacity != 1)
canvas.PushOpacity(Opacity, boundsRect);
if (ClipToBounds && !HandlesClipToBounds)
@ -79,6 +82,9 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopClip();
if (Opacity != 1)
canvas.PopOpacity();
if (Effect != null)
canvas.PopEffect();
}
protected virtual bool HandlesClipToBounds => false;
@ -101,10 +107,18 @@ namespace Avalonia.Rendering.Composition.Server
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
public Matrix4x4 GlobalTransformMatrix { get; private set; }
public virtual void Update(ServerCompositionTarget root)
public record struct UpdateResult(Rect? Bounds, bool InvalidatedOld, bool InvalidatedNew)
{
public UpdateResult() : this(null, false, false)
{
}
}
public virtual UpdateResult Update(ServerCompositionTarget root)
{
if (Parent == null && Root == null)
return;
return default;
var wasVisible = IsVisibleInFrame;
@ -146,6 +160,11 @@ namespace Avalonia.Rendering.Composition.Server
GlobalTransformMatrix = newTransform;
var ownBounds = OwnContentBounds;
// Since padding is applied in the current visual's coordinate space we expand bounds before transforming them
if (Effect != null)
ownBounds = ownBounds.Inflate(Effect.GetEffectOutputPadding());
if (ownBounds != _oldOwnContentBounds || positionChanged)
{
_oldOwnContentBounds = ownBounds;
@ -168,7 +187,7 @@ namespace Avalonia.Rendering.Composition.Server
_combinedTransformedClipBounds =
AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds
?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
?? new Rect(Root!.Size);
if (_transformedClipBounds != null)
@ -208,9 +227,10 @@ namespace Avalonia.Rendering.Composition.Server
readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id;
readback.Visible = IsHitTestVisibleInFrame;
return new(TransformedOwnContentBounds, invalidateNewBounds, invalidateOldBounds);
}
void AddDirtyRect(Rect rc)
protected void AddDirtyRect(Rect rc)
{
if (rc == default)
return;

2
src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs

@ -13,7 +13,7 @@ namespace Avalonia.Rendering.SceneGraph
Custom = custom;
}
public override bool HitTest(Point p) => Custom.HitTest(p);
public override bool HitTestTransformed(Point p) => Custom.HitTest(p);
public override void Render(IDrawingContextImpl context)
{

15
src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs

@ -37,5 +37,20 @@ namespace Avalonia.Rendering.SceneGraph
}
public Matrix Transform { get; }
public sealed override bool HitTest(Point p)
{
if (Transform.IsIdentity)
return HitTestTransformed(p);
if (!Transform.HasInverse)
return false;
var transformedPoint = Transform.Invert().Transform(p);
return HitTestTransformed(transformedPoint);
}
public abstract bool HitTestTransformed(Point p);
}
}

2
src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

@ -43,7 +43,7 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect);
public override bool HitTest(Point p)
public override bool HitTestTransformed(Point p)
{
var center = Rect.Center;

2
src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs

@ -65,6 +65,6 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p);
public override bool HitTestTransformed(Point p) => Rect.Rect.ContainsExclusive(p);
}
}

2
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@ -64,7 +64,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override bool HitTest(Point p)
public override bool HitTestTransformed(Point p)
{
return (Brush != null && Geometry.FillContains(p)) ||
(Pen != null && Geometry.StrokeContains(Pen, p));

5
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -53,7 +53,10 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
public override bool HitTestTransformed(Point p)
{
return GlyphRun.Item.Bounds.ContainsExclusive(p);
}
public override void Dispose()
{

2
src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs

@ -94,7 +94,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override bool HitTest(Point p) => DestRect.ContainsExclusive(p);
public override bool HitTestTransformed(Point p) => DestRect.ContainsExclusive(p);
public override void Dispose()
{

2
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@ -66,7 +66,7 @@ namespace Avalonia.Rendering.SceneGraph
context.DrawLine(Pen, P1, P2);
}
public override bool HitTest(Point p)
public override bool HitTestTransformed(Point p)
{
var halfThickness = Pen.Thickness / 2;
var minX = Math.Min(P1.X, P2.X) - halfThickness;

2
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public override bool HitTest(Point p) => false;
public override bool HitTestTransformed(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.

2
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@ -74,7 +74,7 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
/// <inheritdoc/>
public override bool HitTest(Point p)
public override bool HitTestTransformed(Point p)
{
if (Brush != null)
{

18
src/Avalonia.Base/StyledElement.cs

@ -78,7 +78,7 @@ namespace Avalonia
private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount;
private string? _name;
private readonly Classes _classes = new Classes();
private Classes? _classes;
private ILogicalRoot? _logicalRoot;
private IAvaloniaList<ILogical>? _logicalChildren;
private IResourceDictionary? _resources;
@ -183,21 +183,7 @@ namespace Avalonia
/// collection.
/// </para>
/// </remarks>
public Classes Classes
{
get
{
return _classes;
}
set
{
if (_classes != value)
{
_classes.Replace(value);
}
}
}
public Classes Classes => _classes ??= new();
/// <summary>
/// Gets or sets the control's data context.

8
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@ -557,10 +557,10 @@ public partial class Dispatcher
/// <returns>
/// An task that completes after the task returned from callback finishes
/// </returns>
public Task InvokeTaskAsync(Func<Task> callback, DispatcherPriority priority = default)
public Task InvokeAsync(Func<Task> callback, DispatcherPriority priority = default)
{
_ = callback ?? throw new ArgumentNullException(nameof(callback));
return InvokeAsync(callback, priority).GetTask().Unwrap();
return InvokeAsync<Task>(callback, priority).GetTask().Unwrap();
}
/// <summary>
@ -578,10 +578,10 @@ public partial class Dispatcher
/// <returns>
/// An task that completes after the task returned from callback finishes
/// </returns>
public Task<TResult> InvokeTaskAsync<TResult>(Func<Task<TResult>> action, DispatcherPriority priority = default)
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> action, DispatcherPriority priority = default)
{
_ = action ?? throw new ArgumentNullException(nameof(action));
return InvokeAsync(action, priority).GetTask().Unwrap();
return InvokeAsync<Task<TResult>>(action, priority).GetTask().Unwrap();
}
/// <summary>

20
src/Avalonia.Base/Visual.cs

@ -48,7 +48,7 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<Geometry?> ClipProperty =
AvaloniaProperty.Register<Visual, Geometry?>(nameof(Clip));
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
@ -66,6 +66,12 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
AvaloniaProperty.Register<Visual, IBrush?>(nameof(OpacityMask));
/// <summary>
/// Defines the <see cref="Effect"/> property.
/// </summary>
public static readonly StyledProperty<IEffect?> EffectProperty =
AvaloniaProperty.Register<Visual, IEffect?>(nameof(Effect));
/// <summary>
/// Defines the <see cref="HasMirrorTransform"/> property.
@ -127,6 +133,8 @@ namespace Avalonia
ClipToBoundsProperty,
IsVisibleProperty,
OpacityProperty,
OpacityMaskProperty,
EffectProperty,
HasMirrorTransformProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
ZIndexProperty.Changed.Subscribe(ZIndexChanged);
@ -233,6 +241,16 @@ namespace Avalonia
get { return GetValue(OpacityMaskProperty); }
set { SetValue(OpacityMaskProperty, value); }
}
/// <summary>
/// Gets or sets the effect of the control.
/// </summary>
public IEffect? Effect
{
get => GetValue(EffectProperty);
set => SetValue(EffectProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether to apply mirror transform on this control.

6
src/Avalonia.Base/composition-schema.xml

@ -6,7 +6,8 @@
<Using>Avalonia.Rendering.Composition.Animations</Using>
<Manual Name="Avalonia.Platform.IGeometryImpl" Passthrough="true"/>
<Manual Name="Avalonia.Media.IBrush" Passthrough="true"/>
<Manual Name="Avalonia.Media.IImmutableBrush" Passthrough="true"/>
<Manual Name="Avalonia.Media.IImmutableEffect" Passthrough="true"/>
<Manual Name="CompositionSurface" />
<Manual Name="CompositionDrawingSurface" />
<Object Name="CompositionVisual" Abstract="true">
@ -27,7 +28,8 @@
<Property Name="TransformMatrix" Type="Matrix4x4" DefaultValue="Matrix4x4.Identity" Animated="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IImmutableBrush?" Internal="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">

28
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -3962,6 +3962,7 @@ namespace Avalonia.Controls
bool focusLeftDataGrid = true;
bool dataGridWillReceiveRoutedEvent = true;
Visual focusedObject = FocusManager.Instance.Current as Visual;
DataGridColumn editingColumn = null;
while (focusedObject != null)
{
@ -3974,22 +3975,29 @@ namespace Avalonia.Controls
// Walk up the visual tree. If we hit the root, try using the framework element's
// parent. We do this because Popups behave differently with respect to the visual tree,
// and it could have a parent even if the VisualTreeHelper doesn't find it.
Visual parent = focusedObject.GetVisualParent();
var parent = focusedObject.Parent as Visual;
if (parent == null)
{
if (focusedObject is Control element)
{
parent = element.VisualParent;
if (parent != null)
{
dataGridWillReceiveRoutedEvent = false;
}
}
parent = focusedObject.GetVisualParent();
}
else
{
dataGridWillReceiveRoutedEvent = false;
}
focusedObject = parent;
}
if (focusLeftDataGrid)
if (EditingRow != null && EditingColumnIndex != -1)
{
editingColumn = ColumnsItemsInternal[EditingColumnIndex];
if (focusLeftDataGrid && editingColumn is DataGridTemplateColumn)
{
dataGridWillReceiveRoutedEvent = false;
}
}
if (focusLeftDataGrid && !(editingColumn is DataGridTemplateColumn))
{
ContainsFocus = false;
if (EditingRow != null)

4
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -88,10 +88,10 @@ namespace Avalonia.Automation.Peers
if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy)
{
return labeledBy.GetName();
result = labeledBy.GetName();
}
return null;
return result;
}
protected override AutomationPeer? GetParentCore()

29
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -15,6 +15,7 @@ namespace Avalonia.Controls.Chrome
[PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
public class CaptionButtons : TemplatedControl
{
private Button? _restoreButton;
private IDisposable? _disposables;
/// <summary>
@ -28,14 +29,23 @@ namespace Avalonia.Controls.Chrome
{
HostWindow = hostWindow;
_disposables = HostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":minimized", x == WindowState.Minimized);
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
});
_disposables = new CompositeDisposable
{
HostWindow.GetObservable(Window.CanResizeProperty)
.Subscribe(x =>
{
if (_restoreButton is not null)
_restoreButton.IsEnabled = x;
}),
HostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":minimized", x == WindowState.Minimized);
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
}),
};
}
}
@ -94,6 +104,9 @@ namespace Avalonia.Controls.Chrome
restoreButton.Click += (sender, e) => OnRestore();
minimiseButton.Click += (sender, e) => OnMinimize();
fullScreenButton.Click += (sender, e) => OnToggleFullScreen();
restoreButton.IsEnabled = HostWindow?.CanResize ?? true;
_restoreButton = restoreButton;
}
}
}

2
src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs

@ -454,7 +454,7 @@ namespace Avalonia.Controls.Primitives
children.Add(new ListBoxItem
{
Height = ItemHeight,
Classes = new Classes($"{PanelType}Item"),
Classes = { $"{PanelType}Item" },
VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center,
Focusable = false
});

4
src/Avalonia.Controls/Flyouts/Flyout.cs

@ -13,13 +13,13 @@ namespace Avalonia.Controls
public static readonly StyledProperty<object> ContentProperty =
AvaloniaProperty.Register<Flyout, object>(nameof(Content));
private Classes? _classes;
/// <summary>
/// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting
/// </summary>
public Classes FlyoutPresenterClasses => _classes ??= new Classes();
private Classes? _classes;
/// <summary>
/// Defines the <see cref="FlyoutPresenterTheme"/> property.
/// </summary>

76
src/Avalonia.Controls/TextBlock.cs

@ -663,7 +663,6 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = availableSize.Deflate(padding);
_textLayout?.Dispose();
_textLayout = null;
@ -690,18 +689,18 @@ namespace Avalonia.Controls
inline.BuildTextRun(textRuns);
}
foreach (var textRun in textRuns)
_textRuns = textRuns;
foreach (var textRun in _textRuns)
{
if (textRun is EmbeddedControlRun controlRun &&
controlRun.Control is Control control)
controlRun.Control is Control control)
{
VisualChildren.Add(control);
control.Measure(Size.Infinity);
}
}
_textRuns = textRuns;
}
var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
@ -711,64 +710,39 @@ namespace Avalonia.Controls
protected override Size ArrangeOverride(Size finalSize)
{
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if (finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(textWidth);
}
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
if (HasComplexContent)
{
ArrangeComplexContent(TextLayout, padding);
}
var scale = LayoutHelper.GetLayoutScale(this);
if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width))
{
return finalSize;
}
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
var currentY = padding.Top;
_textLayout?.Dispose();
_textLayout = null;
if (HasComplexContent)
{
ArrangeComplexContent(TextLayout, padding);
}
return finalSize;
}
private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding)
{
var currentY = padding.Top;
foreach (var textLine in textLayout.TextLines)
{
var currentX = padding.Left + textLine.Start;
foreach (var run in textLine.TextRuns)
foreach (var textLine in TextLayout.TextLines)
{
if (run is DrawableTextRun drawable)
var currentX = padding.Left + textLine.Start;
foreach (var run in textLine.TextRuns)
{
if (drawable is EmbeddedControlRun controlRun
&& controlRun.Control is Control control)
if (run is DrawableTextRun drawable)
{
control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
if (drawable is EmbeddedControlRun controlRun
&& controlRun.Control is Control control)
{
control.Arrange(
new Rect(new Point(currentX, currentY),
new Size(control.DesiredSize.Width, textLine.Height)));
}
currentX += drawable.Size.Width;
}
currentX += drawable.Size.Width;
}
}
currentY += textLine.Height;
currentY += textLine.Height;
}
}
return finalSize;
}
protected override AutomationPeer OnCreateAutomationPeer()

2
src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs

@ -98,7 +98,7 @@ namespace Avalonia.Native
var surroundingText = _client.SurroundingText;
_inputMethod.SetSurroundingText(
surroundingText.Text,
surroundingText.Text ?? "",
surroundingText.AnchorOffset,
surroundingText.CursorOffset
);

12
src/Avalonia.Native/WindowImpl.cs

@ -21,6 +21,7 @@ namespace Avalonia.Native
private DoubleClickHelper _doubleClickHelper;
private readonly ITopLevelNativeMenuExporter _nativeMenuExporter;
private readonly AvaloniaNativeTextInputMethod _inputMethod;
private bool _canResize = true;
internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts,
AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature)
@ -75,6 +76,7 @@ namespace Avalonia.Native
public void CanResize(bool value)
{
_canResize = value;
_native.SetCanResize(value.AsComBool());
}
@ -137,14 +139,10 @@ namespace Avalonia.Native
{
if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position))
{
// TOGGLE WINDOW STATE.
if (WindowState == WindowState.Maximized || WindowState == WindowState.FullScreen)
if (_canResize)
{
WindowState = WindowState.Normal;
}
else
{
WindowState = WindowState.Maximized;
WindowState = WindowState is WindowState.Maximized or WindowState.FullScreen ?
WindowState.Normal : WindowState.Maximized;
}
}
else

12
src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml

@ -48,7 +48,8 @@
</Viewbox>
</Button>
<Button x:Name="PART_MinimiseButton"
Theme="{StaticResource FluentCaptionButton}">
Theme="{StaticResource FluentCaptionButton}"
AutomationProperties.Name="Minimise">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{TemplateBinding Foreground}"
@ -56,7 +57,8 @@
</Viewbox>
</Button>
<Button x:Name="PART_RestoreButton"
Theme="{StaticResource FluentCaptionButton}">
Theme="{StaticResource FluentCaptionButton}"
AutomationProperties.Name="Maximise">
<Viewbox Width="11" Margin="2">
<Viewbox.RenderTransform>
<RotateTransform Angle="-90" />
@ -70,7 +72,8 @@
<Button x:Name="PART_CloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource FluentCaptionButton}">
Theme="{StaticResource FluentCaptionButton}"
AutomationProperties.Name="Close">
<Viewbox Width="11" Margin="2">
<Path Stretch="UniformToFill"
Fill="{TemplateBinding Foreground}"
@ -93,5 +96,8 @@
<Style Selector="^:fullscreen /template/ Button#PART_MinimiseButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ Button#PART_RestoreButton:disabled">
<Setter Property="Opacity" Value="0.2"/>
</Style>
</ControlTheme>
</ResourceDictionary>

12
src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml

@ -54,7 +54,8 @@
</Viewbox>
</Button>
<Button x:Name="PART_MinimiseButton"
Theme="{StaticResource SimpleCaptionButton}">
Theme="{StaticResource SimpleCaptionButton}"
AutomationProperties.Name="Minimise">
<Viewbox Width="11"
Margin="2">
<Path Data="M2048 1229v-205h-2048v205h2048z"
@ -63,7 +64,8 @@
</Viewbox>
</Button>
<Button x:Name="PART_RestoreButton"
Theme="{StaticResource SimpleCaptionButton}">
Theme="{StaticResource SimpleCaptionButton}"
AutomationProperties.Name="Maximise">
<Viewbox Width="11"
Margin="2">
<Viewbox.RenderTransform>
@ -78,7 +80,8 @@
<Button x:Name="PART_CloseButton"
Background="#ffe81123"
BorderBrush="#fff1707a"
Theme="{StaticResource SimpleCaptionButton}">
Theme="{StaticResource SimpleCaptionButton}"
AutomationProperties.Name="Close">
<Viewbox Width="11"
Margin="2">
<Path Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z"
@ -102,5 +105,8 @@
<Style Selector="^:fullscreen /template/ Button#PART_MinimiseButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ Button#PART_RestoreButton:disabled">
<Setter Property="Opacity" Value="0.2"/>
</Style>
</ControlTheme>
</ResourceDictionary>

2
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -90,6 +90,8 @@ namespace Avalonia.Browser
_topLevel.Renderer.Start();
InputHelper.InitializeBackgroundHandlers();
InputHelper.SubscribeKeyEvents(
_containerElement,
OnKeyDown,

5
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -81,7 +81,10 @@ internal static partial class InputHelper
[JSImport("InputHelper.setBounds", AvaloniaModule.MainModuleName)]
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
[JSImport("globalThis.navigator.clipboard.readText")]
[JSImport("InputHelper.initializeBackgroundHandlers", AvaloniaModule.MainModuleName)]
public static partial void InitializeBackgroundHandlers();
[JSImport("InputHelper.readClipboardText", AvaloniaModule.MainModuleName)]
public static partial Task<string> ReadClipboardTextAsync();
[JSImport("globalThis.navigator.clipboard.writeText")]

4
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -14,11 +14,11 @@ namespace Avalonia.Browser
private bool _signaled;
private static KeyboardDevice? s_keyboard;
public IWindowImpl CreateWindow() => throw new NotSupportedException();
public IWindowImpl CreateWindow() => throw new NotSupportedException("Browser doesn't support windowing platform. In order to display a single-view content, set ISingleViewApplicationLifetime.MainView.");
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow()
{
throw new NotImplementedException();
throw new NotImplementedException("Browser doesn't support embeddable windowing platform.");
}
public ITrayIconImpl? CreateTrayIcon()

59
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@ -19,14 +19,68 @@ enum RawInputModifiers {
PenBarrelButton = 2048
}
/*
* This is a hack to handle Mozilla clipboard events in a more convinient way for framework users.
* In the browser, events go in order KeyDown -> Paste -> KeyUp.
* On KeyDown we trigger Avalonia handlers, which might execute readClipboardText.
* When readClipboardText was executed, we mark ClipboardState as Pending and setup clipboard promise,
* which will un-handle KeyDown event, basically allowing browser to pass a Paste event properly.
* On actual Paste event we execute promise callbacks, resuming async operation, and returning pasted text to the app.
* Note #1, on every KeyUp event we will reset all the state and reject pending promises if any, as this event it expected to come after Paste.
* Note #2, whole this code will be executed only on legacy browsers like Mozilla, where clipboard.readText is not available.
* Note #3, with all of these hacks Clipboard.ReadText will still work only on actual "paste" gesture initiated by user.
* */
enum ClipboardState {
None,
Ready,
Pending
}
export class InputHelper {
static clipboardState: ClipboardState = ClipboardState.None;
static resolveClipboard?: any;
static rejectClipboard?: any;
public static initializeBackgroundHandlers() {
if (this.clipboardState !== ClipboardState.None) {
return;
}
globalThis.addEventListener("paste", (args: any) => {
if (this.clipboardState === ClipboardState.Pending) {
this.resolveClipboard(args.clipboardData.getData("text"));
}
});
this.clipboardState = ClipboardState.Ready;
}
public static async readClipboardText(): Promise<string> {
if (globalThis.navigator.clipboard.readText) {
return await globalThis.navigator.clipboard.readText();
} else {
try {
return await new Promise<any>((resolve, reject) => {
this.clipboardState = ClipboardState.Pending;
this.resolveClipboard = resolve;
this.rejectClipboard = reject;
});
} finally {
this.clipboardState = ClipboardState.Ready;
this.resolveClipboard = null;
this.rejectClipboard = null;
}
}
}
public static subscribeKeyEvents(
element: HTMLInputElement,
keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean,
keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean) {
const keyDownHandler = (args: KeyboardEvent) => {
if (keyDownCallback(args.code, args.key, this.getModifiers(args))) {
args.preventDefault();
if (this.clipboardState !== ClipboardState.Pending) {
args.preventDefault();
}
}
};
element.addEventListener("keydown", keyDownHandler);
@ -35,6 +89,9 @@ export class InputHelper {
if (keyUpCallback(args.code, args.key, this.getModifiers(args))) {
args.preventDefault();
}
if (this.rejectClipboard) {
this.rejectClipboard();
}
};
element.addEventListener("keyup", keyUpHandler);

3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -43,7 +43,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter<PropertyReferenceResolver>(
new AvaloniaXamlIlAvaloniaPropertyResolver(),
new AvaloniaXamlIlReorderClassesPropertiesTransformer()
new AvaloniaXamlIlReorderClassesPropertiesTransformer(),
new AvaloniaXamlIlClassesTransformer()
);
InsertBefore<ContentConvertTransformer>(

9
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -221,15 +221,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result);
}
if (type.Equals(types.Classes))
{
var classes = text.Split(' ');
var classNodes = classes.Select(c => new XamlAstTextNode(node, c, type: types.XamlIlTypes.String)).ToArray();
result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, types.Classes, types.XamlIlTypes.String, classNodes);
return true;
}
if (types.IBrush.IsAssignableFrom(type))
{
if (Color.TryParse(text, out Color color))

46
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXAmlIlClassesTransformer.cs

@ -0,0 +1,46 @@
using System.Linq;
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
/// <summary>
/// Converts an attribute syntax property value assignment to a collection syntax property
/// assignment.
/// </summary>
/// <remarks>
/// Converts the property assignment `Classes="foo bar"` to:
///
/// <code>
/// <StyledElement.Classes>
/// <x:String>foo</String>
/// <x:String>bar</String>
/// </StyledElement.Classes>
/// </code>
/// </remarks>
class AvaloniaXamlIlClassesTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
var types = context.GetAvaloniaTypes();
if (node is XamlAstXamlPropertyValueNode propertyValue &&
propertyValue.IsAttributeSyntax &&
propertyValue.Property is XamlAstClrProperty property &&
property.Getter?.ReturnType.Equals(types.Classes) == true &&
propertyValue.Values.Count == 1 &&
propertyValue.Values[0] is XamlAstTextNode value)
{
var classes = value.Text.Split(' ');
var stringType = context.Configuration.WellKnownTypes.String;
return new XamlAstXamlPropertyValueNode(
node,
property,
classes.Select(x => new XamlAstTextNode(node, x, type: stringType)),
false);
}
return node;
}
}
}

3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs

@ -65,7 +65,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
AllowMultiple = true,
AllowXNull = allowNull,
AllowRuntimeNull = allowNull
AllowRuntimeNull = allowNull,
AllowAttributeSyntax = false,
};
}

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs

@ -37,7 +37,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
on.Children[c] = new XamlAstXamlPropertyValueNode(ch,
new XamlAstNamePropertyReference(ch,
new XamlAstXmlTypeReference(ch, AvaloniaNs, "Design"),
mapTo, on.Type), directive.Values);
mapTo, on.Type), directive.Values, true);
c++;
}
}

3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs

@ -52,7 +52,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
new XamlAstNamePropertyReference(d,
new XamlAstClrTypeReference(ch, declaringType, false), dataTypeProperty.Name,
on.Type),
d.Values);
d.Values,
true);
}
}
}

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs

@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
on.Children[c] = new XamlAstXamlPropertyValueNode(d,
new XamlAstNamePropertyReference(d, on.Type, "Name", on.Type),
d.Values);
d.Values, true);
}
}

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit 5e498f8bcca403a34aff5efc825cbb4e12b7fa8e
Subproject commit 5dd0b042e144e677638224c49fec16dab66143e8

50
src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs

@ -0,0 +1,50 @@
using System;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia;
partial class DrawingContextImpl
{
public void PushEffect(IEffect effect)
{
CheckLease();
using var filter = CreateEffect(effect);
var paint = SKPaintCache.Shared.Get();
paint.ImageFilter = filter;
Canvas.SaveLayer(paint);
SKPaintCache.Shared.ReturnReset(paint);
}
public void PopEffect()
{
CheckLease();
Canvas.Restore();
}
SKImageFilter? CreateEffect(IEffect effect)
{
if (effect is IBlurEffect blur)
{
if (blur.Radius <= 0)
return null;
var sigma = SkBlurRadiusToSigma(blur.Radius);
return SKImageFilter.CreateBlur(sigma, sigma);
}
if (effect is IDropShadowEffect drop)
{
var sigma = drop.BlurRadius > 0 ? SkBlurRadiusToSigma(drop.BlurRadius) : 0;
var alpha = drop.Color.A * drop.Opacity;
if (!_useOpacitySaveLayer)
alpha *= _currentOpacity;
var color = new SKColor(drop.Color.R, drop.Color.G, drop.Color.B, (byte)Math.Max(0, Math.Min(255, alpha)));
return SKImageFilter.CreateDropShadow((float)drop.OffsetX, (float)drop.OffsetY, sigma, sigma, color);
}
return null;
}
}

16
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -19,7 +19,9 @@ namespace Avalonia.Skia
/// <summary>
/// Skia based drawing context.
/// </summary>
internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
internal partial class DrawingContextImpl : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport,
IDrawingContextImplWithEffects
{
private IDisposable?[]? _disposables;
private readonly Vector _dpi;
@ -249,6 +251,12 @@ namespace Avalonia.Skia
}
}
private static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
private struct BoxShadowFilter : IDisposable
{
public readonly SKPaint Paint;
@ -262,12 +270,6 @@ namespace Avalonia.Skia
ClipOperation = clipOperation;
}
private static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity)
{
var ac = shadow.Color;

37
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -731,36 +731,25 @@ namespace Avalonia.Win32
}
case WindowsMessage.WM_IME_COMPOSITION:
{
var previousComposition = Imm32InputMethod.Current.Composition;
var flags = (GCS)ToInt32(lParam);
var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR);
if ((flags & GCS.GCS_COMPSTR) != 0)
{
var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR);
Imm32InputMethod.Current.CompositionChanged(currentComposition);
Imm32InputMethod.Current.CompositionChanged(currentComposition);
}
switch (flags)
if ((flags & GCS.GCS_RESULTSTR) != 0)
{
case GCS.GCS_RESULTSTR:
{
if(!string.IsNullOrEmpty(previousComposition) && ToInt32(wParam) >= 32)
{
Imm32InputMethod.Current.Composition = previousComposition;
var result = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_RESULTSTR);
_ignoreWmChar = true;
}
break;
}
case GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE:
{
// Chinese IME sends WM_CHAR after composition has finished.
break;
}
case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE:
{
// Japanese IME sends WM_CHAR after composition has finished.
break;
}
if (!string.IsNullOrEmpty(result))
{
Imm32InputMethod.Current.Composition = result;
_ignoreWmChar = true;
}
}
break;

4
src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs

@ -20,9 +20,9 @@ internal class NameDirectiveTransformer : IXamlAstTransformer
objectNode.Children[index] = new XamlAstXamlPropertyValueNode(
directive,
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type),
directive.Values);
directive.Values, true);
}
return node;
}
}
}

73
tests/Avalonia.Base.UnitTests/Media/EffectTests.cs

@ -0,0 +1,73 @@
using System;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Base.UnitTests.Media;
public class EffectTests
{
[Fact]
public void Parse_Parses_Blur()
{
var effect = (ImmutableBlurEffect)Effect.Parse("blur(123.34)");
Assert.Equal(123.34, effect.Radius);
}
private const uint Black = 0xff000000;
[Theory,
InlineData("drop-shadow(10 20)", 10, 20, 0, Black),
InlineData("drop-shadow( 10 20 ) ", 10, 20, 0, Black),
InlineData("drop-shadow( 10 20 30 ) ", 10, 20, 30, Black),
InlineData("drop-shadow(10 20 30)", 10, 20, 30, Black),
InlineData("drop-shadow(-10 -20 30)", -10, -20, 30, Black),
InlineData("drop-shadow(10 20 30 #ffff00ff)", 10, 20, 30, 0xffff00ff),
InlineData("drop-shadow ( 10 20 30 #ffff00ff ) ", 10, 20, 30, 0xffff00ff),
InlineData("drop-shadow(10 20 30 red)", 10, 20, 30, 0xffff0000),
InlineData("drop-shadow ( 10 20 30 red ) ", 10, 20, 30, 0xffff0000),
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%))", 10, 20, 30, 0x90641e2d),
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%) ) ", 10, 20, 30, 0x90641e2d),
]
public void Parse_Parses_DropShadow(string s, double x, double y, double r, uint color)
{
var effect = (ImmutableDropShadowEffect)Effect.Parse(s);
Assert.Equal(x, effect.OffsetX);
Assert.Equal(y, effect.OffsetY);
Assert.Equal(r, effect.BlurRadius);
Assert.Equal(1, effect.Opacity);
}
[Theory,
InlineData("blur"),
InlineData("blur("),
InlineData("blur()"),
InlineData("blur(123"),
InlineData("blur(aaab)"),
InlineData("drop-shadow(-10 -20 -30)"),
]
public void Invalid_Effect_Parse_Fails(string b)
{
Assert.Throws<ArgumentException>(() => Effect.Parse(b));
}
[Theory,
InlineData("blur(2.5)", 4, 4, 4, 4),
InlineData("blur(0)", 0, 0, 0, 0),
InlineData("drop-shadow(10 15)", 0, 0, 10, 15),
InlineData("drop-shadow(10 15 5)", 0, 0, 16, 21),
InlineData("drop-shadow(0 0 5)", 6, 6, 6, 6),
InlineData("drop-shadow(3 3 5)", 3, 3, 9, 9)
]
public static void PaddingIsCorrectlyCalculated(string effect, double left, double top, double right, double bottom)
{
var padding = Effect.Parse(effect).GetEffectOutputPadding();
Assert.Equal(left, padding.Left);
Assert.Equal(top, padding.Top);
Assert.Equal(right, padding.Right);
Assert.Equal(bottom, padding.Bottom);
}
}

18
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@ -74,6 +74,22 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
geometryNode.HitTest(new Point());
}
[Fact]
public void HitTest_RectangleNode_With_Transform_Hits()
{
var geometry = Mock.Of<IGeometryImpl>();
var geometryNode = new RectangleNode(
Matrix.CreateTranslation(20,20),
Brushes.Black,
null,
new RoundedRect(new Rect(0,0,10,10)),
default);
var actual = geometryNode.HitTest(new Point(25,25));
Assert.True(actual);
}
private class TestRectangleDrawOperation : RectangleNode
{
public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen)
@ -82,7 +98,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
}
public override bool HitTest(Point p) => false;
public override bool HitTestTransformed(Point p) => false;
public override void Render(IDrawingContextImpl context) { }
}

58
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@ -1,11 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Interactions;
@ -249,6 +246,37 @@ namespace Avalonia.IntegrationTests.Appium
Assert.Equal(new Rgba32(255, 0, 0), centerColor);
}
[Theory]
[InlineData(ShowWindowMode.NonOwned, true)]
[InlineData(ShowWindowMode.Owned, true)]
[InlineData(ShowWindowMode.Modal, true)]
[InlineData(ShowWindowMode.NonOwned, false)]
[InlineData(ShowWindowMode.Owned, false)]
[InlineData(ShowWindowMode.Modal, false)]
public void Window_Has_Disabled_Maximize_Button_When_CanResize_Is_False(ShowWindowMode mode, bool extendClientArea)
{
using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false, extendClientArea: extendClientArea))
{
var secondaryWindow = GetWindow("SecondaryWindow");
AppiumWebElement? maximizeButton;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
maximizeButton = extendClientArea ?
secondaryWindow.FindElementByXPath("//Button[@Name='Maximise']") :
secondaryWindow.FindElementByXPath("//TitleBar/Button[2]");
}
else
{
maximizeButton = mode == ShowWindowMode.NonOwned ?
secondaryWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow") :
secondaryWindow.FindElementByAccessibilityId("_XCUI:ZoomWindow");
}
Assert.False(maximizeButton.Enabled);
}
}
public static TheoryData<Size?, ShowWindowMode, WindowStartupLocation, bool> StartupLocationData()
{
var sizes = new Size?[] { null, new Size(400, 300) };
@ -333,7 +361,8 @@ namespace Avalonia.IntegrationTests.Appium
ShowWindowMode mode,
WindowStartupLocation location = WindowStartupLocation.Manual,
WindowState state = Controls.WindowState.Normal,
bool canResize = true)
bool canResize = true,
bool extendClientArea = false)
{
var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode");
@ -341,7 +370,8 @@ namespace Avalonia.IntegrationTests.Appium
var stateComboBox = _session.FindElementByAccessibilityId("ShowWindowState");
var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize");
var showButton = _session.FindElementByAccessibilityId("ShowWindow");
var extendClientAreaCheckBox = _session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint");
if (size.HasValue)
sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}");
@ -366,9 +396,27 @@ namespace Avalonia.IntegrationTests.Appium
if (canResizeCheckBox.GetIsChecked() != canResize)
canResizeCheckBox.Click();
if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
extendClientAreaCheckBox.Click();
return showButton.OpenWindowWithClick();
}
private AppiumWebElement GetWindow(string identifier)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed
// but in the meantime use the `parent::' selector to return the parent "real" window.
return _session.FindElementByXPath(
$"XCUIElementTypeWindow//*[@identifier='{identifier}']/parent::XCUIElementTypeWindow");
}
else
{
return _session.FindElementByXPath($"//Window[@AutomationId='{identifier}']");
}
}
private WindowInfo GetWindowInfo()
{
PixelRect? ReadOwnerRect()

16
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@ -336,22 +336,6 @@ namespace Avalonia.IntegrationTests.Appium
secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click();
}
[PlatformTheory(TestPlatforms.MacOS)]
[InlineData(ShowWindowMode.NonOwned)]
[InlineData(ShowWindowMode.Owned)]
[InlineData(ShowWindowMode.Modal)]
public void Window_Has_Disabled_Zoom_Button_When_CanResize_Is_False(ShowWindowMode mode)
{
using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false))
{
var secondaryWindow = GetWindow("SecondaryWindow");
var zoomButton = mode == ShowWindowMode.NonOwned ?
secondaryWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow") :
secondaryWindow.FindElementByAccessibilityId("_XCUI:ZoomWindow");
Assert.False(zoomButton.Enabled);
}
}
[PlatformFact(TestPlatforms.MacOS)]
public void Toggling_SystemDecorations_Should_Preserve_ExtendClientArea()
{

60
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -1,24 +1,20 @@
using System;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Presenters;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Metadata;
using Avalonia.Styling;
using Avalonia.UnitTests;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Xunit;
using Avalonia.Controls.Documents;
using Avalonia.Metadata;
using Avalonia.Themes.Simple;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
@ -910,7 +906,49 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("World", target.Test2);
Assert.Equal("Hello", target.Test1);
}
[Fact]
public void Can_Specify_Button_Classes()
{
var xaml = "<Button xmlns='https://github.com/avaloniaui' Classes='foo bar'/>";
var target = (Button)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new[] { "foo", "bar" }, target.Classes);
}
[Fact]
public void Can_Specify_Button_Classes_Longform()
{
var xaml = @"
<Button xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button.Classes>
<x:String>foo</x:String>
<x:String>bar</x:String>
</Button.Classes>
</Button>";
var target = (Button)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new[] { "foo", "bar" }, target.Classes);
}
[Fact]
public void Can_Specify_Flyout_FlyoutPresenterClasses()
{
var xaml = "<Flyout xmlns='https://github.com/avaloniaui' FlyoutPresenterClasses='foo bar'/>";
var target = (Flyout)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new[] { "foo", "bar" }, target.FlyoutPresenterClasses);
}
[Fact]
public void Trying_To_Bind_ItemsControl_Items_Throws()
{
var xaml = "<ItemsControl xmlns='https://github.com/avaloniaui' Items='{Binding}'/>";
Assert.ThrowsAny<XmlException>(() => AvaloniaRuntimeXamlLoader.Load(xaml));
}
private class SelectedItemsViewModel : INotifyPropertyChanged
{
public string[] Items { get; set; }

43
tests/Avalonia.RenderTests/Media/EffectTests.cs

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using Xunit;
#pragma warning disable CS0649
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
public class EffectTests : TestBase
{
public EffectTests() : base(@"Media\Effects")
{
}
[Fact]
public async Task DropShadowEffect()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Border()
{
Background = null,
Margin = new Thickness(40),
Effect = new ImmutableDropShadowEffect(20, 30, 5, Colors.Green, 1),
Child = new Border
{
Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 255)),
BorderBrush = Brushes.Red,
BorderThickness = new Thickness(5)
}
}
};
await RenderToFile(target);
CompareImages(skipImmediate: true);
}
}
#endif

3
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -6,6 +6,9 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.RenderTests\**\*.cs" />
<Compile Update="..\Avalonia.RenderTests\Media\EffectTests.cs">
<Link>Media\EffectTests.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -416,7 +416,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
new[] { "Lorem ipsum ", "dolor sit ", "amet, ", "consectetur ", "adipisicing ", "elit, sed do ", "eiusmod tempor" })]
new[] { "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing ", "elit, sed do ", "eiusmod tempor" })]
[Theory]
public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)

BIN
tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Loading…
Cancel
Save