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