Browse Source

Merge branch 'master' into refactor/resources

pull/3957/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
7b202b71a7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 221
      src/Avalonia.Animation/Animatable.cs
  2. 2
      src/Avalonia.Animation/TransitionInstance.cs
  3. 13
      src/Avalonia.Animation/Transitions.cs
  4. 43
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  5. 15
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  6. 34
      src/Avalonia.Controls/Shapes/Path.cs
  7. 111
      src/Avalonia.Controls/Shapes/Shape.cs
  8. 10
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  9. 2
      src/Avalonia.Visuals/Media/DrawingContext.cs
  10. 22
      src/Avalonia.Visuals/Media/GlyphRun.cs
  11. 49
      src/Avalonia.Visuals/Rect.cs
  12. 3
      src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
  13. 5
      src/Avalonia.Visuals/Visual.cs
  14. 339
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  15. 4
      tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
  16. 88
      tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
  17. 4
      tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
  18. 40
      tests/Avalonia.LeakTests/ControlTests.cs
  19. 45
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  20. 8
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs
  21. 13
      tests/Avalonia.RenderTests/Media/ImageBrushTests.cs
  22. 36
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  23. 2
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  24. 3
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs
  25. 4
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
  26. 19
      tests/Avalonia.Visuals.UnitTests/RectTests.cs
  27. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
  28. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  29. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
  30. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
  31. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
  32. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
  33. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
  34. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
  35. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
  36. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png
  37. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png
  38. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png
  39. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png
  40. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png
  41. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
  42. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  43. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
  44. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
  45. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
  46. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
  47. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
  48. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
  49. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

221
src/Avalonia.Animation/Animatable.cs

@ -1,10 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Collections;
using System.Collections.Specialized;
using Avalonia.Data;
using Avalonia.Animation.Animators;
#nullable enable
namespace Avalonia.Animation
{
@ -13,9 +13,24 @@ namespace Avalonia.Animation
/// </summary>
public class Animatable : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="Clock"/> property.
/// </summary>
public static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
/// <summary>
/// Defines the <see cref="Transitions"/> property.
/// </summary>
public static readonly StyledProperty<Transitions?> TransitionsProperty =
AvaloniaProperty.Register<Animatable, Transitions?>(nameof(Transitions));
private bool _transitionsEnabled = true;
private Dictionary<ITransition, TransitionState>? _transitionState;
/// <summary>
/// Gets or sets the clock which controls the animations on the control.
/// </summary>
public IClock Clock
{
get => GetValue(ClockProperty);
@ -23,68 +38,194 @@ namespace Avalonia.Animation
}
/// <summary>
/// Defines the <see cref="Transitions"/> property.
/// Gets or sets the property transitions for the control.
/// </summary>
public static readonly DirectProperty<Animatable, Transitions> TransitionsProperty =
AvaloniaProperty.RegisterDirect<Animatable, Transitions>(
nameof(Transitions),
o => o.Transitions,
(o, v) => o.Transitions = v);
public Transitions? Transitions
{
get => GetValue(TransitionsProperty);
set => SetValue(TransitionsProperty, value);
}
private Transitions _transitions;
/// <summary>
/// Enables transitions for the control.
/// </summary>
/// <remarks>
/// This method should not be called from user code, it will be called automatically by the framework
/// when a control is added to the visual tree.
/// </remarks>
protected void EnableTransitions()
{
if (!_transitionsEnabled)
{
_transitionsEnabled = true;
private Dictionary<AvaloniaProperty, IDisposable> _previousTransitions;
if (Transitions is object)
{
AddTransitions(Transitions);
}
}
}
/// <summary>
/// Gets or sets the property transitions for the control.
/// Disables transitions for the control.
/// </summary>
public Transitions Transitions
/// <remarks>
/// This method should not be called from user code, it will be called automatically by the framework
/// when a control is added to the visual tree.
/// </remarks>
protected void DisableTransitions()
{
if (_transitionsEnabled)
{
_transitionsEnabled = false;
if (Transitions is object)
{
RemoveTransitions(Transitions);
}
}
}
protected sealed override void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
get
if (change.Property == TransitionsProperty && change.IsEffectiveValueChange)
{
if (_transitions is null)
_transitions = new Transitions();
var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
if (oldTransitions is object)
{
oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
RemoveTransitions(oldTransitions);
}
return _transitions;
if (newTransitions is object)
{
newTransitions.CollectionChanged += TransitionsCollectionChanged;
AddTransitions(newTransitions);
}
}
set
else if (_transitionsEnabled &&
Transitions is object &&
_transitionState is object &&
!change.Property.IsDirect &&
change.Priority > BindingPriority.Animation)
{
if (value is null)
return;
foreach (var transition in Transitions)
{
if (transition.Property == change.Property)
{
var state = _transitionState[transition];
var oldValue = state.BaseValue;
var newValue = GetAnimationBaseValue(transition.Property);
if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
if (!Equals(oldValue, newValue))
{
state.BaseValue = newValue;
SetAndRaise(TransitionsProperty, ref _transitions, value);
// We need to transition from the current animated value if present,
// instead of the old base value.
var animatedValue = GetValue(transition.Property);
if (!Equals(newValue, animatedValue))
{
oldValue = animatedValue;
}
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
Clock ?? AvaloniaLocator.Current.GetService<IGlobalClock>(),
oldValue,
newValue);
return;
}
}
}
}
base.OnPropertyChangedCore(change);
}
private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (!_transitionsEnabled)
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddTransitions(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveTransitions(e.OldItems);
break;
case NotifyCollectionChangedAction.Replace:
RemoveTransitions(e.OldItems);
AddTransitions(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
throw new NotSupportedException("Transitions collection cannot be reset.");
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
private void AddTransitions(IList items)
{
if (_transitions is null || _previousTransitions is null || change.Priority == BindingPriority.Animation)
if (!_transitionsEnabled)
{
return;
}
_transitionState ??= new Dictionary<ITransition, TransitionState>();
// PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
foreach (var transition in _transitions)
for (var i = 0; i < items.Count; ++i)
{
if (transition.Property == change.Property)
var t = (ITransition)items[i];
_transitionState.Add(t, new TransitionState
{
if (_previousTransitions.TryGetValue(change.Property, out var dispose))
dispose.Dispose();
BaseValue = GetAnimationBaseValue(t.Property),
});
}
}
var instance = transition.Apply(
this,
Clock ?? Avalonia.Animation.Clock.GlobalClock,
change.OldValue.GetValueOrDefault(),
change.NewValue.GetValueOrDefault());
private void RemoveTransitions(IList items)
{
if (_transitionState is null)
{
return;
}
_previousTransitions[change.Property] = instance;
return;
for (var i = 0; i < items.Count; ++i)
{
var t = (ITransition)items[i];
if (_transitionState.TryGetValue(t, out var state))
{
state.Instance?.Dispose();
_transitionState.Remove(t);
}
}
}
private object GetAnimationBaseValue(AvaloniaProperty property)
{
var value = this.GetBaseValue(property, BindingPriority.LocalValue);
if (value == AvaloniaProperty.UnsetValue)
{
value = GetValue(property);
}
return value;
}
private class TransitionState
{
public IDisposable? Instance { get; set; }
public object? BaseValue { get; set; }
}
}
}

2
src/Avalonia.Animation/TransitionInstance.cs

@ -19,6 +19,8 @@ namespace Avalonia.Animation
public TransitionInstance(IClock clock, TimeSpan Duration)
{
clock = clock ?? throw new ArgumentNullException(nameof(clock));
_duration = Duration;
_baseClock = clock;
}

13
src/Avalonia.Animation/Transitions.cs

@ -1,4 +1,6 @@
using System;
using Avalonia.Collections;
using Avalonia.Threading;
namespace Avalonia.Animation
{
@ -13,6 +15,17 @@ namespace Avalonia.Animation
public Transitions()
{
ResetBehavior = ResetBehavior.Remove;
Validate = ValidateTransition;
}
private void ValidateTransition(ITransition obj)
{
Dispatcher.UIThread.VerifyAccess();
if (obj.Property.IsDirect)
{
throw new InvalidOperationException("Cannot animate a direct property.");
}
}
}
}

43
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -115,45 +115,46 @@ namespace Avalonia.Utilities
return true;
}
var toUnderl = Nullable.GetUnderlyingType(to) ?? to;
var from = value.GetType();
if (to.IsAssignableFrom(from))
if (toUnderl.IsAssignableFrom(from))
{
result = value;
return true;
}
if (to == typeof(string))
if (toUnderl == typeof(string))
{
result = Convert.ToString(value);
result = Convert.ToString(value, culture);
return true;
}
if (to.IsEnum && from == typeof(string))
if (toUnderl.IsEnum && from == typeof(string))
{
if (Enum.IsDefined(to, (string)value))
if (Enum.IsDefined(toUnderl, (string)value))
{
result = Enum.Parse(to, (string)value);
result = Enum.Parse(toUnderl, (string)value);
return true;
}
}
if (!from.IsEnum && to.IsEnum)
if (!from.IsEnum && toUnderl.IsEnum)
{
result = null;
if (TryConvert(Enum.GetUnderlyingType(to), value, culture, out object enumValue))
if (TryConvert(Enum.GetUnderlyingType(toUnderl), value, culture, out object enumValue))
{
result = Enum.ToObject(to, enumValue);
result = Enum.ToObject(toUnderl, enumValue);
return true;
}
}
if (from.IsEnum && IsNumeric(to))
if (from.IsEnum && IsNumeric(toUnderl))
{
try
{
result = Convert.ChangeType((int)value, to, culture);
result = Convert.ChangeType((int)value, toUnderl, culture);
return true;
}
catch
@ -164,7 +165,7 @@ namespace Avalonia.Utilities
}
var convertableFrom = Array.IndexOf(InbuiltTypes, from);
var convertableTo = Array.IndexOf(InbuiltTypes, to);
var convertableTo = Array.IndexOf(InbuiltTypes, toUnderl);
if (convertableFrom != -1 && convertableTo != -1)
{
@ -172,7 +173,7 @@ namespace Avalonia.Utilities
{
try
{
result = Convert.ChangeType(value, to, culture);
result = Convert.ChangeType(value, toUnderl, culture);
return true;
}
catch
@ -183,15 +184,23 @@ namespace Avalonia.Utilities
}
}
var typeConverter = TypeDescriptor.GetConverter(to);
var toTypeConverter = TypeDescriptor.GetConverter(toUnderl);
if (toTypeConverter.CanConvertFrom(from) == true)
{
result = toTypeConverter.ConvertFrom(null, culture, value);
return true;
}
var fromTypeConverter = TypeDescriptor.GetConverter(from);
if (typeConverter.CanConvertFrom(from) == true)
if (fromTypeConverter.CanConvertTo(toUnderl) == true)
{
result = typeConverter.ConvertFrom(null, culture, value);
result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl);
return true;
}
var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit);
var cast = FindTypeConversionOperatorMethod(from, toUnderl, OperatorType.Implicit | OperatorType.Explicit);
if (cast != null)
{

15
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -75,6 +75,9 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
AffectsRender<TextPresenter>(SelectionBrushProperty);
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
@ -284,8 +287,6 @@ namespace Avalonia.Controls.Presenters
protected void InvalidateFormattedText()
{
_formattedText = null;
InvalidateMeasure();
}
/// <summary>
@ -301,13 +302,15 @@ namespace Avalonia.Controls.Presenters
context.FillRectangle(background, new Rect(Bounds.Size));
}
FormattedText.Constraint = Bounds.Size;
context.DrawText(Foreground, new Point(), FormattedText);
}
public override void Render(DrawingContext context)
{
FormattedText.Constraint = Bounds.Size;
_constraint = Bounds.Size;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
@ -316,10 +319,6 @@ namespace Avalonia.Controls.Presenters
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
// issue #600: set constraint before any FormattedText manipulation
// see base.Render(...) implementation
FormattedText.Constraint = _constraint;
var rects = FormattedText.HitTestTextRange(start, length);
foreach (var rect in rects)

34
src/Avalonia.Controls/Shapes/Path.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Shapes
@ -9,6 +8,8 @@ namespace Avalonia.Controls.Shapes
public static readonly StyledProperty<Geometry> DataProperty =
AvaloniaProperty.Register<Path, Geometry>(nameof(Data));
private EventHandler _geometryChangedHandler;
static Path()
{
AffectsGeometry<Path>(DataProperty);
@ -21,21 +22,48 @@ namespace Avalonia.Controls.Shapes
set { SetValue(DataProperty, value); }
}
private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= GeometryChanged;
protected override Geometry CreateDefiningGeometry() => Data;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (Data is object)
{
Data.Changed += GeometryChangedHandler;
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (Data is object)
{
Data.Changed -= GeometryChangedHandler;
}
}
private void DataChanged(AvaloniaPropertyChangedEventArgs e)
{
if (VisualRoot is null)
{
return;
}
var oldGeometry = (Geometry)e.OldValue;
var newGeometry = (Geometry)e.NewValue;
if (oldGeometry is object)
{
oldGeometry.Changed -= GeometryChanged;
oldGeometry.Changed -= GeometryChangedHandler;
}
if (newGeometry is object)
{
newGeometry.Changed += GeometryChanged;
newGeometry.Changed += GeometryChangedHandler;
}
}

111
src/Avalonia.Controls/Shapes/Shape.cs

@ -2,38 +2,67 @@ using System;
using Avalonia.Collections;
using Avalonia.Media;
#nullable enable
namespace Avalonia.Controls.Shapes
{
/// <summary>
/// Provides a base class for shape elements, such as <see cref="Ellipse"/>, <see cref="Polygon"/> and <see cref="Rectangle"/>.
/// </summary>
public abstract class Shape : Control
{
public static readonly StyledProperty<IBrush> FillProperty =
AvaloniaProperty.Register<Shape, IBrush>(nameof(Fill));
/// <summary>
/// Defines the <see cref="Fill"/> property.
/// </summary>
public static readonly StyledProperty<IBrush?> FillProperty =
AvaloniaProperty.Register<Shape, IBrush?>(nameof(Fill));
/// <summary>
/// Defines the <see cref="Stretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> StretchProperty =
AvaloniaProperty.Register<Shape, Stretch>(nameof(Stretch));
public static readonly StyledProperty<IBrush> StrokeProperty =
AvaloniaProperty.Register<Shape, IBrush>(nameof(Stroke));
/// <summary>
/// Defines the <see cref="Stroke"/> property.
/// </summary>
public static readonly StyledProperty<IBrush?> StrokeProperty =
AvaloniaProperty.Register<Shape, IBrush?>(nameof(Stroke));
public static readonly StyledProperty<AvaloniaList<double>> StrokeDashArrayProperty =
AvaloniaProperty.Register<Shape, AvaloniaList<double>>(nameof(StrokeDashArray));
/// <summary>
/// Defines the <see cref="StrokeDashArray"/> property.
/// </summary>
public static readonly StyledProperty<AvaloniaList<double>?> StrokeDashArrayProperty =
AvaloniaProperty.Register<Shape, AvaloniaList<double>?>(nameof(StrokeDashArray));
/// <summary>
/// Defines the <see cref="StrokeDashOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> StrokeDashOffsetProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeDashOffset));
/// <summary>
/// Defines the <see cref="StrokeThickness"/> property.
/// </summary>
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeThickness));
/// <summary>
/// Defines the <see cref="StrokeLineCap"/> property.
/// </summary>
public static readonly StyledProperty<PenLineCap> StrokeLineCapProperty =
AvaloniaProperty.Register<Shape, PenLineCap>(nameof(StrokeLineCap), PenLineCap.Flat);
/// <summary>
/// Defines the <see cref="StrokeJoin"/> property.
/// </summary>
public static readonly StyledProperty<PenLineJoin> StrokeJoinProperty =
AvaloniaProperty.Register<Shape, PenLineJoin>(nameof(StrokeJoin), PenLineJoin.Miter);
private Matrix _transform = Matrix.Identity;
private Geometry _definingGeometry;
private Geometry _renderedGeometry;
bool _calculateTransformOnArrange = false;
private Geometry? _definingGeometry;
private Geometry? _renderedGeometry;
private bool _calculateTransformOnArrange;
static Shape()
{
@ -43,7 +72,10 @@ namespace Avalonia.Controls.Shapes
StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty);
}
public Geometry DefiningGeometry
/// <summary>
/// Gets a value that represents the <see cref="Geometry"/> of the shape.
/// </summary>
public Geometry? DefiningGeometry
{
get
{
@ -56,13 +88,10 @@ namespace Avalonia.Controls.Shapes
}
}
public IBrush Fill
{
get { return GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
public Geometry RenderedGeometry
/// <summary>
/// Gets a value that represents the final rendered <see cref="Geometry"/> of the shape.
/// </summary>
public Geometry? RenderedGeometry
{
get
{
@ -93,42 +122,72 @@ namespace Avalonia.Controls.Shapes
}
}
/// <summary>
/// Gets or sets the <see cref="IBrush"/> that specifies how the shape's interior is painted.
/// </summary>
public IBrush? Fill
{
get { return GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="Stretch"/> enumeration value that describes how the shape fills its allocated space.
/// </summary>
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
public IBrush Stroke
/// <summary>
/// Gets or sets the <see cref="IBrush"/> that specifies how the shape's outline is painted.
/// </summary>
public IBrush? Stroke
{
get { return GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
}
public AvaloniaList<double> StrokeDashArray
/// <summary>
/// Gets or sets a collection of <see cref="double"/> values that indicate the pattern of dashes and gaps that is used to outline shapes.
/// </summary>
public AvaloniaList<double>? StrokeDashArray
{
get { return GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); }
}
/// <summary>
/// Gets or sets a value that specifies the distance within the dash pattern where a dash begins.
/// </summary>
public double StrokeDashOffset
{
get { return GetValue(StrokeDashOffsetProperty); }
set { SetValue(StrokeDashOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the width of the shape outline.
/// </summary>
public double StrokeThickness
{
get { return GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineCap"/> enumeration value that describes the shape at the ends of a line.
/// </summary>
public PenLineCap StrokeLineCap
{
get { return GetValue(StrokeLineCapProperty); }
set { SetValue(StrokeLineCapProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineJoin"/> enumeration value that specifies the type of join that is used at the vertices of a Shape.
/// </summary>
public PenLineJoin StrokeJoin
{
get { return GetValue(StrokeJoinProperty); }
@ -170,12 +229,20 @@ namespace Avalonia.Controls.Shapes
}
}
protected abstract Geometry CreateDefiningGeometry();
/// <summary>
/// Creates the shape's defining geometry.
/// </summary>
/// <returns>Defining <see cref="Geometry"/> of the shape.</returns>
protected abstract Geometry? CreateDefiningGeometry();
/// <summary>
/// Invalidates the geometry of this shape.
/// </summary>
protected void InvalidateGeometry()
{
_renderedGeometry = null;
_definingGeometry = null;
InvalidateMeasure();
}
@ -321,8 +388,8 @@ namespace Avalonia.Controls.Shapes
// portion changes.
if (e.Property == BoundsProperty)
{
var oldBounds = (Rect)e.OldValue;
var newBounds = (Rect)e.NewValue;
var oldBounds = (Rect)e.OldValue!;
var newBounds = (Rect)e.NewValue!;
if (oldBounds.Size == newBounds.Size)
{

10
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@ -24,6 +24,7 @@ namespace Avalonia.Styling
private BindingValue<T> _value;
private IDisposable? _subscription;
private IDisposable? _subscriptionTwoWay;
private IDisposable? _innerSubscription;
private bool _isActive;
public PropertySetterBindingInstance(
@ -121,6 +122,9 @@ namespace Avalonia.Styling
sub.Dispose();
}
_innerSubscription?.Dispose();
_innerSubscription = null;
base.Dispose();
}
@ -144,13 +148,13 @@ namespace Avalonia.Styling
protected override void Subscribed()
{
_subscription = _binding.Observable.Subscribe(_inner);
_innerSubscription = _binding.Observable.Subscribe(_inner);
}
protected override void Unsubscribed()
{
_subscription?.Dispose();
_subscription = null;
_innerSubscription?.Dispose();
_innerSubscription = null;
}
private void PublishNext()

2
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -350,7 +350,7 @@ namespace Avalonia.Media
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
PushedState PushSetTransform(Matrix matrix)
public PushedState PushSetTransform(Matrix matrix)
{
var oldMatrix = CurrentTransform;
CurrentTransform = matrix;

22
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -44,7 +44,6 @@ namespace Avalonia.Media
/// <param name="characters">The characters.</param>
/// <param name="glyphClusters">The glyph clusters.</param>
/// <param name="biDiLevel">The bidi level.</param>
/// <param name="bounds">The bound.</param>
public GlyphRun(
GlyphTypeface glyphTypeface,
double fontRenderingEmSize,
@ -53,8 +52,7 @@ namespace Avalonia.Media
ReadOnlySlice<Vector> glyphOffsets = default,
ReadOnlySlice<char> characters = default,
ReadOnlySlice<ushort> glyphClusters = default,
int biDiLevel = 0,
Rect? bounds = null)
int biDiLevel = 0)
{
GlyphTypeface = glyphTypeface;
@ -71,8 +69,6 @@ namespace Avalonia.Media
GlyphClusters = glyphClusters;
BiDiLevel = biDiLevel;
Initialize(bounds);
}
/// <summary>
@ -182,7 +178,7 @@ namespace Avalonia.Media
{
if (_glyphRunImpl == null)
{
Initialize(null);
Initialize();
}
return _glyphRunImpl;
@ -517,8 +513,7 @@ namespace Avalonia.Media
/// <summary>
/// Initializes the <see cref="GlyphRun"/>.
/// </summary>
/// <param name="bounds">Optional pre computed bounds.</param>
private void Initialize(Rect? bounds)
private void Initialize()
{
if (GlyphIndices.Length == 0)
{
@ -541,16 +536,9 @@ namespace Avalonia.Media
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width);
if (bounds.HasValue)
{
_bounds = bounds;
}
else
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
_bounds = new Rect(0, 0, width, height);
}
_bounds = new Rect(0, 0, width, height);
}
void IDisposable.Dispose()

49
src/Avalonia.Visuals/Rect.cs

@ -421,11 +421,50 @@ namespace Avalonia
}
/// <summary>
/// Gets the union of two rectangles.
/// </summary>
/// <param name="rect">The other rectangle.</param>
/// <returns>The union.</returns>
public Rect Union(Rect rect)
/// Normalizes the rectangle so both the <see cref="Width"/> and <see
/// cref="Height"/> are positive, without changing the location of the rectangle
/// </summary>
/// <returns>Normalized Rect</returns>
/// <remarks>
/// Empty rect will be return when Rect contains invalid values. Like NaN.
/// </remarks>
public Rect Normalize()
{
Rect rect = this;
if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) ||
double.IsNaN(rect.X) || double.IsNaN(rect.Y) ||
double.IsNaN(Height) || double.IsNaN(Width))
{
return Rect.Empty;
}
if (rect.Width < 0)
{
var x = X + Width;
var width = X - x;
rect = rect.WithX(x).WithWidth(width);
}
if (rect.Height < 0)
{
var y = Y + Height;
var height = Y - y;
rect = rect.WithY(y).WithHeight(height);
}
return rect;
}
/// <summary>
/// Gets the union of two rectangles.
/// </summary>
/// <param name="rect">The other rectangle.</param>
/// <returns>The union.</returns>
public Rect Union(Rect rect)
{
if (IsEmpty)
{

3
src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs

@ -11,7 +11,8 @@ namespace Avalonia.Rendering.SceneGraph
{
public DrawOperation(Rect bounds, Matrix transform)
{
bounds = bounds.TransformToAABB(transform);
bounds = bounds.Normalize().TransformToAABB(transform);
Bounds = new Rect(
new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)),
new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom)));

5
src/Avalonia.Visuals/Visual.cs

@ -114,6 +114,9 @@ namespace Avalonia
/// </summary>
public Visual()
{
// Disable transitions until we're added to the visual tree.
DisableTransitions();
var visualChildren = new AvaloniaList<IVisual>();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.Validate = visual => ValidateVisualChild(visual);
@ -393,6 +396,7 @@ namespace Avalonia
RenderTransform.Changed += RenderTransformChanged;
}
EnableTransitions();
OnAttachedToVisualTree(e);
AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual();
@ -429,6 +433,7 @@ namespace Avalonia
RenderTransform.Changed -= RenderTransformChanged;
}
DisableTransitions();
OnDetachedFromVisualTree(e);
DetachedFromVisualTree?.Invoke(this, e);
e.Root?.Renderer?.AddDirty(this);

339
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@ -0,0 +1,339 @@
using System;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Animation.UnitTests
{
public class AnimatableTests
{
[Fact]
public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree()
{
var target = CreateTarget();
var control = new Control
{
Transitions = new Transitions { target.Object },
};
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5),
Times.Never);
}
[Fact]
public void Transition_Is_Not_Applied_To_Initial_Style()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
var target = CreateTarget();
var control = new Control
{
Transitions = new Transitions { target.Object },
};
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Control>())
{
Setters =
{
new Setter(Visual.OpacityProperty, 0.8),
}
}
}
};
root.Child = control;
Assert.Equal(0.8, control.Opacity);
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
}
[Fact]
public void Transition_Is_Applied_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
}
[Fact]
public void Transition_Is_Not_Applied_When_Animated_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation);
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5),
Times.Never);
}
[Fact]
public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.SetValue(Visual.OpacityProperty, 0.5);
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
target.ResetCalls();
control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger);
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
[Fact]
public void Transition_Is_Disposed_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(control, It.IsAny<IClock>(), 1.0, 0.5)).Returns(sub.Object);
control.Opacity = 0.5;
sub.ResetCalls();
control.Opacity = 0.4;
sub.Verify(x => x.Dispose());
}
[Fact]
public void New_Transition_Is_Applied_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
target.Setup(x => x.Property).Returns(Visual.OpacityProperty);
target.Setup(x => x.Apply(control, It.IsAny<IClock>(), 1.0, 0.5))
.Callback(() =>
{
control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation);
})
.Returns(Mock.Of<IDisposable>());
control.Opacity = 0.5;
Assert.Equal(0.9, control.Opacity);
target.ResetCalls();
control.Opacity = 0.4;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
0.9,
0.4));
}
[Fact]
public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
target.ResetCalls();
var root = (TestRoot)control.Parent;
root.Child = null;
control.Opacity = 0.8;
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
[Fact]
public void Animation_Is_Cancelled_When_Transition_Removed()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(
It.IsAny<Animatable>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>())).Returns(sub.Object);
control.Opacity = 0.5;
control.Transitions.RemoveAt(0);
sub.Verify(x => x.Dispose());
}
[Fact]
public void Animation_Is_Cancelled_When_New_Style_Activates()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
var target = CreateTarget();
var control = CreateStyledControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5)).Returns(sub.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<Clock>(),
1.0,
0.5),
Times.Once);
control.Classes.Add("foo");
sub.Verify(x => x.Dispose());
}
}
[Fact]
public void Transition_From_Style_Trigger_Is_Applied()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
{
var target = CreateTransition(Control.WidthProperty);
var control = CreateStyledControl(transition2: target.Object);
var sub = new Mock<IDisposable>();
control.Classes.Add("foo");
control.Width = 100;
target.Verify(x => x.Apply(
control,
It.IsAny<Clock>(),
double.NaN,
100.0),
Times.Once);
}
}
private static Mock<ITransition> CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);
}
private static Control CreateControl(ITransition transition)
{
var control = new Control
{
Transitions = new Transitions { transition },
};
var root = new TestRoot(control);
return control;
}
private static Control CreateStyledControl(
ITransition transition1 = null,
ITransition transition2 = null)
{
transition1 = transition1 ?? CreateTarget().Object;
transition2 = transition2 ?? CreateTransition(Control.WidthProperty).Object;
var control = new Control
{
Styles =
{
new Style(x => x.OfType<Control>())
{
Setters =
{
new Setter
{
Property = Control.TransitionsProperty,
Value = new Transitions { transition1 },
}
}
},
new Style(x => x.OfType<Control>().Class("foo"))
{
Setters =
{
new Setter
{
Property = Control.TransitionsProperty,
Value = new Transitions { transition2 },
}
}
}
}
};
var root = new TestRoot(control);
return control;
}
private static Mock<ITransition> CreateTransition(AvaloniaProperty property)
{
var target = new Mock<ITransition>();
var sub = new Mock<IDisposable>();
target.Setup(x => x.Property).Returns(property);
target.Setup(x => x.Apply(
It.IsAny<Animatable>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>())).Returns(sub.Object);
return target;
}
}
}

4
tests/Avalonia.Animation.UnitTests/TransitionsTests.cs

@ -16,7 +16,7 @@ namespace Avalonia.Animation.UnitTests
{
var border = new Border
{
Transitions =
Transitions = new Transitions
{
new DoubleTransition
{
@ -44,7 +44,7 @@ namespace Avalonia.Animation.UnitTests
{
var border = new Border
{
Transitions =
Transitions = new Transitions
{
new DoubleTransition
{

88
tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs

@ -6,6 +6,7 @@ using System.Windows.Input;
using System;
using Avalonia.Data.Converters;
using Avalonia.Layout;
using System.ComponentModel;
namespace Avalonia.Base.UnitTests.Data.Converters
{
@ -35,6 +36,54 @@ namespace Avalonia.Base.UnitTests.Data.Converters
Assert.Equal(5.0, result);
}
[Fact]
public void Do_Not_Throw_On_InvalidInput_For_NullableInt()
{
var result = DefaultValueConverter.Instance.Convert(
"<not-a-number>",
typeof(int?),
null,
CultureInfo.InvariantCulture);
Assert.IsType(typeof(BindingNotification), result);
}
[Fact]
public void Can_Convert_Decimal_To_NullableDouble()
{
var result = DefaultValueConverter.Instance.Convert(
5m,
typeof(double?),
null,
CultureInfo.InvariantCulture);
Assert.Equal(5.0, result);
}
[Fact]
public void Can_Convert_CustomType_To_Int()
{
var result = DefaultValueConverter.Instance.Convert(
new CustomType(123),
typeof(int),
null,
CultureInfo.InvariantCulture);
Assert.Equal(123, result);
}
[Fact]
public void Can_Convert_Int_To_CustomType()
{
var result = DefaultValueConverter.Instance.Convert(
123,
typeof(CustomType),
null,
CultureInfo.InvariantCulture);
Assert.Equal(new CustomType(123), result);
}
[Fact]
public void Can_Convert_String_To_Enum()
{
@ -187,5 +236,44 @@ namespace Avalonia.Base.UnitTests.Data.Converters
return v.Value;
}
}
[TypeConverter(typeof(CustomTypeConverter))]
private class CustomType {
public int Value { get; }
public CustomType(int value)
{
Value = value;
}
public override bool Equals(object obj)
{
return obj is CustomType other && this.Value == other.Value;
}
}
private class CustomTypeConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(int);
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(int);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
return ((CustomType)value).Value;
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return new CustomType((int)value);
}
}
}
}

4
tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs

@ -23,12 +23,16 @@ namespace Avalonia.Controls.UnitTests.Shapes
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
var target = new Path { Data = geometry };
var root = new TestRoot(target);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
geometry.Rect = new Rect(0, 0, 20, 20);
Assert.False(target.IsMeasureValid);
root.Child = null;
}
}
}

40
tests/Avalonia.LeakTests/ControlTests.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics;
using Avalonia.Input;
@ -492,6 +493,45 @@ namespace Avalonia.LeakTests
}
}
[Fact]
public void Path_Is_Freed()
{
using (Start())
{
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
Func<Window> run = () =>
{
var window = new Window
{
Content = new Path
{
Data = geometry
}
};
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.IsType<Path>(window.Presenter.Child);
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Path>()).ObjectsCount));
// We are keeping geometry alive to simulate a resource that outlives the control.
GC.KeepAlive(geometry);
}
}
private IDisposable Start()
{
return UnitTestApplication.Start(TestServices.StyledWindow.With(

45
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -337,5 +337,50 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Brushes.Red, listBox.Background);
}
}
[Fact]
public void Transitions_Can_Be_Styled()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border'>
<Setter Property='Transitions'>
<Transitions>
<DoubleTransition Property='Width' Duration='0:0:1'/>
</Transitions>
</Setter>
</Style>
<Style Selector='Border.foo'>
<Setter Property='Transitions'>
<Transitions>
<DoubleTransition Property='Height' Duration='0:0:1'/>
</Transitions>
</Setter>
</Style>
</Window.Styles>
<Border/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var border = (Border)window.Content;
Assert.Equal(1, border.Transitions.Count);
Assert.Equal(Border.WidthProperty, border.Transitions[0].Property);
border.Classes.Add("foo");
Assert.Equal(1, border.Transitions.Count);
Assert.Equal(Border.HeightProperty, border.Transitions[0].Property);
border.Classes.Remove("foo");
Assert.Equal(1, border.Transitions.Count);
Assert.Equal(Border.WidthProperty, border.Transitions[0].Property);
}
}
}
}

8
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

@ -34,9 +34,11 @@ namespace Avalonia.Markup.Xaml.UnitTests
var parsed = (Grid)AvaloniaXamlLoader.Parse(@"
<Grid xmlns='https://github.com/avaloniaui' >
<Grid.Transitions>
<DoubleTransition Property='Opacity'
Easing='CircularEaseIn'
Duration='0:0:0.5' />
<Transitions>
<DoubleTransition Property='Opacity'
Easing='CircularEaseIn'
Duration='0:0:0.5' />
</Transitions>
</Grid.Transitions>
</Grid>");
Assert.Equal(1, parsed.Transitions.Count);

13
tests/Avalonia.RenderTests/Media/ImageBrushTests.cs

@ -192,11 +192,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact]
#endif
public async Task ImageBrush_Fill_NoTile()
{
Decorator target = new Decorator
@ -219,11 +216,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact]
#endif
public async Task ImageBrush_Uniform_NoTile()
{
Decorator target = new Decorator
@ -246,11 +239,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
#if AVALONIA_SKIA_SKIP_FAIL
[Fact(Skip = "FIXME")]
#else
[Fact]
#endif
public async Task ImageBrush_UniformToFill_NoTile()
{
Decorator target = new Decorator

36
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@ -14,6 +14,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
public class VisualBrushTests : TestBase
{
//Whitespaces are used here to be able to compare rendering results in a platform independent way.
//Otherwise tests will fail because of slightly different glyph rendering.
private static readonly string s_visualBrushText = " ";
public VisualBrushTests()
: base(@"Media\VisualBrush")
{
@ -48,7 +52,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
FontFamily = TestFontFamily,
Background = Brushes.Green,
Foreground = Brushes.Yellow,
Text = "VisualBrush",
Text = s_visualBrushText
}
}
}
@ -56,7 +60,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
}
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_NoTile_Alignment_TopLeft()
{
Decorator target = new Decorator
@ -81,7 +85,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_NoTile_Alignment_Center()
{
Decorator target = new Decorator
@ -106,7 +110,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_NoTile_Alignment_BottomRight()
{
Decorator target = new Decorator
@ -131,7 +135,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_Fill_NoTile()
{
Decorator target = new Decorator
@ -154,7 +158,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_Uniform_NoTile()
{
Decorator target = new Decorator
@ -177,7 +181,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_UniformToFill_NoTile()
{
Decorator target = new Decorator
@ -200,7 +204,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource()
{
Decorator target = new Decorator
@ -224,7 +228,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterDest()
{
Decorator target = new Decorator
@ -248,7 +252,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest()
{
Decorator target = new Decorator
@ -273,7 +277,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_Tile_BottomRightQuarterSource_CenterQuarterDest()
{
Decorator target = new Decorator
@ -298,7 +302,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_FlipX_TopLeftDest()
{
Decorator target = new Decorator
@ -322,7 +326,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_FlipY_TopLeftDest()
{
Decorator target = new Decorator
@ -346,7 +350,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_NoStretch_FlipXY_TopLeftDest()
{
Decorator target = new Decorator
@ -370,7 +374,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
CompareImages();
}
[Fact(Skip = "Visual brush is broken in combination with text rendering.")]
[Fact]
public async Task VisualBrush_InTree_Visual()
{
Border source;
@ -391,7 +395,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Child = new TextBlock
{
FontFamily = TestFontFamily,
Text = "Visual"
Text = s_visualBrushText
}
}),
new Border

2
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@ -508,7 +508,7 @@ namespace Avalonia.Skia.UnitTests
private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
[Fact]
[Fact(Skip= "Only used for profiling.")]
public void Should_Wrap()
{
using (Start())

3
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -30,8 +30,7 @@ namespace Avalonia.UnitTests
width += glyphTypeface.GetGlyphAdvance(glyph);
}
return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, glyphIndices, characters: text,
bounds: new Rect(0, 0, width, height));
return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, glyphIndices, characters: text);
}
}
}

4
tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

@ -128,10 +128,8 @@ namespace Avalonia.Visuals.UnitTests.Media
var characters = new ReadOnlySlice<char>(new char[count], start, count);
var bounds = new Rect(0, 0, count * 10, 10);
return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances,
glyphClusters: glyphClusters, characters: characters, biDiLevel: bidiLevel, bounds: bounds);
glyphClusters: glyphClusters, characters: characters, biDiLevel: bidiLevel);
}
}
}

19
tests/Avalonia.Visuals.UnitTests/RectTests.cs

@ -35,5 +35,24 @@ namespace Avalonia.Visuals.UnitTests
Assert.Equal(new Rect(0, 0, 100, 100), result);
}
[Fact]
public void Normalize_Should_Reverse_Negative_Size()
{
var result = new Rect(new Point(100, 100), new Point(0, 0)).Normalize();
Assert.Equal(new Rect(0, 0, 100, 100), result);
}
[Fact]
public void Normalize_Should_Make_Invalid_Rects_Empty()
{
var result = new Rect(
double.NegativeInfinity, double.PositiveInfinity,
double.PositiveInfinity, double.PositiveInfinity)
.Normalize();
Assert.Equal(Rect.Empty, result);
}
}
}

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 111 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 375 B

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 375 B

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Loading…
Cancel
Save