diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 324ff06452..9e9b84537b 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/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 /// public class Animatable : AvaloniaObject { + /// + /// Defines the property. + /// public static readonly StyledProperty ClockProperty = AvaloniaProperty.Register(nameof(Clock), inherits: true); + /// + /// Defines the property. + /// + public static readonly StyledProperty TransitionsProperty = + AvaloniaProperty.Register(nameof(Transitions)); + + private bool _transitionsEnabled = true; + private Dictionary? _transitionState; + + /// + /// Gets or sets the clock which controls the animations on the control. + /// public IClock Clock { get => GetValue(ClockProperty); @@ -23,68 +38,194 @@ namespace Avalonia.Animation } /// - /// Defines the property. + /// Gets or sets the property transitions for the control. /// - public static readonly DirectProperty TransitionsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Transitions), - o => o.Transitions, - (o, v) => o.Transitions = v); + public Transitions? Transitions + { + get => GetValue(TransitionsProperty); + set => SetValue(TransitionsProperty, value); + } - private Transitions _transitions; + /// + /// Enables transitions for the control. + /// + /// + /// 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. + /// + protected void EnableTransitions() + { + if (!_transitionsEnabled) + { + _transitionsEnabled = true; - private Dictionary _previousTransitions; + if (Transitions is object) + { + AddTransitions(Transitions); + } + } + } /// - /// Gets or sets the property transitions for the control. + /// Disables transitions for the control. /// - public Transitions Transitions + /// + /// 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. + /// + protected void DisableTransitions() + { + if (_transitionsEnabled) + { + _transitionsEnabled = false; + + if (Transitions is object) + { + RemoveTransitions(Transitions); + } + } + } + + protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { - get + if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) { - if (_transitions is null) - _transitions = new Transitions(); + var oldTransitions = change.OldValue.GetValueOrDefault(); + var newTransitions = change.NewValue.GetValueOrDefault(); - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); + 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(); + 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(), + 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(AvaloniaPropertyChangedEventArgs change) + private void AddTransitions(IList items) { - if (_transitions is null || _previousTransitions is null || change.Priority == BindingPriority.Animation) + if (!_transitionsEnabled) + { return; + } + + _transitionState ??= new Dictionary(); - // 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; } + } } } diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index efbbed51b5..ad2001d621 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/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; } diff --git a/src/Avalonia.Animation/Transitions.cs b/src/Avalonia.Animation/Transitions.cs index 2741039ebc..6687a2902d 100644 --- a/src/Avalonia.Animation/Transitions.cs +++ b/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."); + } } } } diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 66b4676b45..d0d88166a7 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/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) { diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 9cbde72f7f..09f86f462c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -75,6 +75,9 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { AffectsRender(SelectionBrushProperty); + AffectsMeasure(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(); } /// @@ -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) diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs index 3fd84c0c7b..d0ffc27d20 100644 --- a/src/Avalonia.Controls/Shapes/Path.cs +++ b/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 DataProperty = AvaloniaProperty.Register(nameof(Data)); + private EventHandler _geometryChangedHandler; + static Path() { AffectsGeometry(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; } } diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 371b5d92f7..7d1525afc4 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -2,38 +2,67 @@ using System; using Avalonia.Collections; using Avalonia.Media; +#nullable enable + namespace Avalonia.Controls.Shapes { + /// + /// Provides a base class for shape elements, such as , and . + /// public abstract class Shape : Control { - public static readonly StyledProperty FillProperty = - AvaloniaProperty.Register(nameof(Fill)); + /// + /// Defines the property. + /// + public static readonly StyledProperty FillProperty = + AvaloniaProperty.Register(nameof(Fill)); + /// + /// Defines the property. + /// public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(nameof(Stretch)); - public static readonly StyledProperty StrokeProperty = - AvaloniaProperty.Register(nameof(Stroke)); + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeProperty = + AvaloniaProperty.Register(nameof(Stroke)); - public static readonly StyledProperty> StrokeDashArrayProperty = - AvaloniaProperty.Register>(nameof(StrokeDashArray)); + /// + /// Defines the property. + /// + public static readonly StyledProperty?> StrokeDashArrayProperty = + AvaloniaProperty.Register?>(nameof(StrokeDashArray)); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeDashOffsetProperty = AvaloniaProperty.Register(nameof(StrokeDashOffset)); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register(nameof(StrokeThickness)); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeLineCapProperty = AvaloniaProperty.Register(nameof(StrokeLineCap), PenLineCap.Flat); + /// + /// Defines the property. + /// public static readonly StyledProperty StrokeJoinProperty = AvaloniaProperty.Register(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 + /// + /// Gets a value that represents the of the shape. + /// + 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 + /// + /// Gets a value that represents the final rendered of the shape. + /// + public Geometry? RenderedGeometry { get { @@ -93,42 +122,72 @@ namespace Avalonia.Controls.Shapes } } + /// + /// Gets or sets the that specifies how the shape's interior is painted. + /// + public IBrush? Fill + { + get { return GetValue(FillProperty); } + set { SetValue(FillProperty, value); } + } + + /// + /// Gets or sets a enumeration value that describes how the shape fills its allocated space. + /// public Stretch Stretch { get { return GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } - public IBrush Stroke + /// + /// Gets or sets the that specifies how the shape's outline is painted. + /// + public IBrush? Stroke { get { return GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } } - public AvaloniaList StrokeDashArray + /// + /// Gets or sets a collection of values that indicate the pattern of dashes and gaps that is used to outline shapes. + /// + public AvaloniaList? StrokeDashArray { get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } } + /// + /// Gets or sets a value that specifies the distance within the dash pattern where a dash begins. + /// public double StrokeDashOffset { get { return GetValue(StrokeDashOffsetProperty); } set { SetValue(StrokeDashOffsetProperty, value); } } + /// + /// Gets or sets the width of the shape outline. + /// public double StrokeThickness { get { return GetValue(StrokeThicknessProperty); } set { SetValue(StrokeThicknessProperty, value); } } + /// + /// Gets or sets a enumeration value that describes the shape at the ends of a line. + /// public PenLineCap StrokeLineCap { get { return GetValue(StrokeLineCapProperty); } set { SetValue(StrokeLineCapProperty, value); } } + /// + /// Gets or sets a enumeration value that specifies the type of join that is used at the vertices of a Shape. + /// public PenLineJoin StrokeJoin { get { return GetValue(StrokeJoinProperty); } @@ -170,12 +229,20 @@ namespace Avalonia.Controls.Shapes } } - protected abstract Geometry CreateDefiningGeometry(); + /// + /// Creates the shape's defining geometry. + /// + /// Defining of the shape. + protected abstract Geometry? CreateDefiningGeometry(); + /// + /// Invalidates the geometry of this shape. + /// 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) { diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs index f975862892..e177993d13 100644 --- a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -24,6 +24,7 @@ namespace Avalonia.Styling private BindingValue _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() diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 1a45b8342a..6fdcd9631b 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -350,7 +350,7 @@ namespace Avalonia.Media /// /// The matrix /// A disposable used to undo the transformation. - PushedState PushSetTransform(Matrix matrix) + public PushedState PushSetTransform(Matrix matrix) { var oldMatrix = CurrentTransform; CurrentTransform = matrix; diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 86c6a7bf2d..29c9d93560 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -44,7 +44,6 @@ namespace Avalonia.Media /// The characters. /// The glyph clusters. /// The bidi level. - /// The bound. public GlyphRun( GlyphTypeface glyphTypeface, double fontRenderingEmSize, @@ -53,8 +52,7 @@ namespace Avalonia.Media ReadOnlySlice glyphOffsets = default, ReadOnlySlice characters = default, ReadOnlySlice 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); } /// @@ -182,7 +178,7 @@ namespace Avalonia.Media { if (_glyphRunImpl == null) { - Initialize(null); + Initialize(); } return _glyphRunImpl; @@ -517,8 +513,7 @@ namespace Avalonia.Media /// /// Initializes the . /// - /// Optional pre computed bounds. - 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() diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index b0c4cb62eb..d1110e0613 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -421,11 +421,50 @@ namespace Avalonia } /// - /// Gets the union of two rectangles. - /// - /// The other rectangle. - /// The union. - public Rect Union(Rect rect) + /// Normalizes the rectangle so both the and are positive, without changing the location of the rectangle + /// + /// Normalized Rect + /// + /// Empty rect will be return when Rect contains invalid values. Like NaN. + /// + 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; + } + + + /// + /// Gets the union of two rectangles. + /// + /// The other rectangle. + /// The union. + public Rect Union(Rect rect) { if (IsEmpty) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index 0b04b97ff2..c49e7705e0 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/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))); diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 36b72fa28e..bb9a4cf208 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -114,6 +114,9 @@ namespace Avalonia /// public Visual() { + // Disable transitions until we're added to the visual tree. + DisableTransitions(); + var visualChildren = new AvaloniaList(); 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); diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs new file mode 100644 index 0000000000..b5c61883e7 --- /dev/null +++ b/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(), + 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()) + { + Setters = + { + new Setter(Visual.OpacityProperty, 0.8), + } + } + } + }; + + root.Child = control; + + Assert.Equal(0.8, control.Opacity); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + 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(), + 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(), + 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(), + 1.0, + 0.5)); + target.ResetCalls(); + + control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger); + + target.Verify(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Transition_Is_Disposed_When_Local_Value_Changes() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply(control, It.IsAny(), 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(), 1.0, 0.5)) + .Callback(() => + { + control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation); + }) + .Returns(Mock.Of()); + + control.Opacity = 0.5; + + Assert.Equal(0.9, control.Opacity); + target.ResetCalls(); + + control.Opacity = 0.4; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 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(), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public void Animation_Is_Cancelled_When_Transition_Removed() + { + var target = CreateTarget(); + var control = CreateControl(target.Object); + var sub = new Mock(); + + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).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(); + + target.Setup(x => x.Apply( + control, + It.IsAny(), + 1.0, + 0.5)).Returns(sub.Object); + + control.Opacity = 0.5; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + 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(); + + control.Classes.Add("foo"); + control.Width = 100; + + target.Verify(x => x.Apply( + control, + It.IsAny(), + double.NaN, + 100.0), + Times.Once); + } + } + + private static Mock 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()) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition1 }, + } + } + }, + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Control.TransitionsProperty, + Value = new Transitions { transition2 }, + } + } + } + } + }; + + var root = new TestRoot(control); + return control; + } + + private static Mock CreateTransition(AvaloniaProperty property) + { + var target = new Mock(); + var sub = new Mock(); + + target.Setup(x => x.Property).Returns(property); + target.Setup(x => x.Apply( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns(sub.Object); + + return target; + } + } +} diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index 22f3b4f501..70ffd781a1 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/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 { diff --git a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs index f70769ac39..efa81dcc1b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs +++ b/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( + "", + 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); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs index 5a9ca410e4..88c64e76cc 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs +++ b/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; } } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 0afb2465ee..9bb9fd7145 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/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 run = () => + { + var window = new Window + { + Content = new Path + { + Data = geometry + } + }; + + window.Show(); + + window.LayoutManager.ExecuteInitialLayoutPass(window); + Assert.IsType(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()).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( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 02f0d7072c..9642f5719d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/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 = @" + + + + + + +"; + 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); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 4ff9e3db38..a408069cb0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -34,9 +34,11 @@ namespace Avalonia.Markup.Xaml.UnitTests var parsed = (Grid)AvaloniaXamlLoader.Parse(@" - + + + "); Assert.Equal(1, parsed.Transitions.Count); diff --git a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs index 8b6a3f017f..09564cfe83 100644 --- a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs +++ b/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 diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index 98b20e9e0d..42ec392066 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/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 diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs index 627b7c2ead..0d9fd31e52 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/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()) diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index de1842b692..0772e0e9bd 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/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); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs index 5d6d830a43..028caa35c6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs @@ -128,10 +128,8 @@ namespace Avalonia.Visuals.UnitTests.Media var characters = new ReadOnlySlice(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); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/RectTests.cs b/tests/Avalonia.Visuals.UnitTests/RectTests.cs index 34953e5fd7..a2b0569949 100644 --- a/tests/Avalonia.Visuals.UnitTests/RectTests.cs +++ b/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); + } } } diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png index 6638456643..3996782889 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png index 3725f2acbc..f93814556e 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png index e6b0098c25..84b76e795e 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png index c64b88dc68..f2346a64a0 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png index a6e8b0688c..cb355b9ce0 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png index 5d75c9cc6c..08f41b3ef5 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png index 8664cafbdf..a0b6afeeeb 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png index fefd9c0d30..1d620ae100 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png index ca7d498ba4..f366912df5 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png index 73d147bf77..6fa53f6f82 100644 Binary files a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Fill_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png index 53e51f0424..5ef32152df 100644 Binary files a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png index 32bdfd44d2..b54b55358f 100644 Binary files a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png index e173fa6cee..22bca987ff 100644 Binary files a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_UniformToFill_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png index de73af8170..abb33c3121 100644 Binary files a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Uniform_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png index 6638456643..078bca57bc 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png index 3725f2acbc..f93814556e 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png index e6b0098c25..84b76e795e 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png index c64b88dc68..f2346a64a0 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png index a6e8b0688c..cb355b9ce0 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png index 5d75c9cc6c..08f41b3ef5 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png index 8664cafbdf..a0b6afeeeb 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png index fefd9c0d30..1f1ac05853 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png index ca7d498ba4..152c703f93 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png differ