Browse Source

Merge branch 'Direct2D1LocatorRemoval' of https://github.com/Gillibald/Avalonia into Direct2D1LocatorRemoval

pull/1861/head
Benedikt Schroeder 8 years ago
parent
commit
d60f528d0a
  1. 3
      samples/ControlCatalog/Pages/CanvasPage.xaml
  2. 2
      samples/ControlCatalog/SideBar.xaml
  3. 2
      samples/RenderDemo/SideBar.xaml
  4. 10
      samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
  5. 2
      samples/interop/Direct3DInteropSample/MainWindow.cs
  6. 44
      src/Avalonia.Animation/Animatable.cs
  7. 86
      src/Avalonia.Animation/Animation.cs
  8. 228
      src/Avalonia.Animation/AnimationInstance`1.cs
  9. 1
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  10. 273
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  11. 90
      src/Avalonia.Animation/Animator`1.cs
  12. 5
      src/Avalonia.Animation/Cue.cs
  13. 79
      src/Avalonia.Animation/Timing.cs
  14. 54
      src/Avalonia.Animation/TransitionInstance.cs
  15. 19
      src/Avalonia.Animation/Transition`1.cs
  16. 16
      src/Avalonia.Animation/Utils/DoubleUtils.cs
  17. 2
      src/Avalonia.Animation/Utils/EasingUtils.cs
  18. 4
      src/Avalonia.Controls/Border.cs
  19. 2
      src/Avalonia.Controls/Button.cs
  20. 6
      src/Avalonia.Controls/ButtonSpinner.cs
  21. 26
      src/Avalonia.Controls/Canvas.cs
  22. 4
      src/Avalonia.Controls/ContentControl.cs
  23. 2
      src/Avalonia.Controls/Decorator.cs
  24. 4
      src/Avalonia.Controls/DockPanel.cs
  25. 6
      src/Avalonia.Controls/DrawingPresenter.cs
  26. 10
      src/Avalonia.Controls/Expander.cs
  27. 5
      src/Avalonia.Controls/Grid.cs
  28. 3
      src/Avalonia.Controls/Image.cs
  29. 42
      src/Avalonia.Controls/Panel.cs
  30. 4
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  31. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  32. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  33. 2
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  34. 2
      src/Avalonia.Controls/Primitives/AccessText.cs
  35. 4
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  36. 6
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  37. 4
      src/Avalonia.Controls/Primitives/Track.cs
  38. 6
      src/Avalonia.Controls/ProgressBar.cs
  39. 5
      src/Avalonia.Controls/Shapes/Shape.cs
  40. 2
      src/Avalonia.Controls/Slider.cs
  41. 4
      src/Avalonia.Controls/StackPanel.cs
  42. 2
      src/Avalonia.Controls/TabControl.cs
  43. 9
      src/Avalonia.Controls/TextBlock.cs
  44. 2
      src/Avalonia.Controls/TopLevel.cs
  45. 2
      src/Avalonia.Controls/WrapPanel.cs
  46. 6
      src/Avalonia.Input/InputElement.cs
  47. 68
      src/Avalonia.Layout/Layoutable.cs
  48. 45
      src/Avalonia.Styling/StyledElement.cs
  49. 10
      src/Avalonia.Themes.Default/CheckBox.xaml
  50. 8
      src/Avalonia.Themes.Default/TreeView.xaml
  51. 8
      src/Avalonia.Visuals/Animation/CrossFade.cs
  52. 14
      src/Avalonia.Visuals/Animation/PageSlide.cs
  53. 11
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  54. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  55. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  56. 36
      src/Avalonia.Visuals/Visual.cs
  57. 40
      src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
  58. 30
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  59. 13
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
  60. 24
      tests/Avalonia.Controls.UnitTests/DockPanelTests.cs
  61. 26
      tests/Avalonia.Controls.UnitTests/GridTests.cs
  62. 37
      tests/Avalonia.RenderTests/OpacityMaskTests.cs
  63. BIN
      tests/TestFiles/Direct2D1/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png
  64. BIN
      tests/TestFiles/Skia/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png

3
samples/ControlCatalog/Pages/CanvasPage.xaml

@ -11,7 +11,8 @@
<GradientStop Offset="1" Color="Transparent"/> <GradientStop Offset="1" Color="Transparent"/>
</LinearGradientBrush.GradientStops> </LinearGradientBrush.GradientStops>
</LinearGradientBrush> </LinearGradientBrush>
</Rectangle.OpacityMask> </Rectangle> </Rectangle.OpacityMask>
</Rectangle>
<Ellipse Fill="Green" Width="58" Height="58" Canvas.Left="88" Canvas.Top="100"/> <Ellipse Fill="Green" Width="58" Height="58" Canvas.Left="88" Canvas.Top="100"/>
<Path Fill="Orange" Data="M 0,0 c 0,0 50,0 50,-50 c 0,0 50,0 50,50 h -50 v 50 l -50,-50 Z" Canvas.Left="30" Canvas.Top="250"/> <Path Fill="Orange" Data="M 0,0 c 0,0 50,0 50,-50 c 0,0 50,0 50,50 h -50 v 50 l -50,-50 Z" Canvas.Left="30" Canvas.Top="250"/>
<Path Fill="OrangeRed" Canvas.Left="180" Canvas.Top="250"> <Path Fill="OrangeRed" Canvas.Left="180" Canvas.Top="250">

2
samples/ControlCatalog/SideBar.xaml

@ -36,7 +36,7 @@
<Setter Property="Opacity" Value="0.5"/> <Setter Property="Opacity" Value="0.5"/>
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.5"/> <DoubleTransition Property="Opacity" Duration="0:0:0.2"/>
</Transitions> </Transitions>
</Setter> </Setter>
</Style> </Style>

2
samples/RenderDemo/SideBar.xaml

@ -37,7 +37,7 @@
<Setter Property="Opacity" Value="0.5"/> <Setter Property="Opacity" Value="0.5"/>
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.5"/> <DoubleTransition Property="Opacity" Duration="0:0:0.2"/>
</Transitions> </Transitions>
</Setter> </Setter>
</Style> </Style>

10
samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs

@ -10,21 +10,21 @@ namespace RenderDemo.ViewModels
public AnimationsPageViewModel() public AnimationsPageViewModel()
{ {
ToggleGlobalPlayState = ReactiveCommand.Create(()=>TogglePlayState()); ToggleGlobalPlayState = ReactiveCommand.Create(() => TogglePlayState());
} }
void TogglePlayState() void TogglePlayState()
{ {
switch (Timing.GetGlobalPlayState()) switch (Animation.GlobalPlayState)
{ {
case PlayState.Run: case PlayState.Run:
PlayStateText = "Resume all animations"; PlayStateText = "Resume all animations";
Timing.SetGlobalPlayState(PlayState.Pause); Animation.GlobalPlayState = PlayState.Pause;
break; break;
case PlayState.Pause: case PlayState.Pause:
PlayStateText = "Pause all animations"; PlayStateText = "Pause all animations";
Timing.SetGlobalPlayState(PlayState.Run); Animation.GlobalPlayState = PlayState.Run;
break; break;
} }
} }
@ -36,5 +36,5 @@ namespace RenderDemo.ViewModels
} }
public ReactiveCommand ToggleGlobalPlayState { get; } public ReactiveCommand ToggleGlobalPlayState { get; }
} }
} }

2
samples/interop/Direct3DInteropSample/MainWindow.cs

@ -132,7 +132,7 @@ namespace Direct3DInteropSample
signature, signature,
inputElements); inputElements);
// Instantiate Vertex buiffer from vertex data // Instantiate Vertex buffer from vertex data
var vertices = Buffer.Create( var vertices = Buffer.Create(
device, device,
BindFlags.VertexBuffer, BindFlags.VertexBuffer,

44
src/Avalonia.Animation/Animatable.cs

@ -11,35 +11,10 @@ using Avalonia.Data;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Base class for control which can have property transitions. /// Base class for all animatable objects.
/// </summary> /// </summary>
public class Animatable : AvaloniaObject public class Animatable : AvaloniaObject
{ {
/// <summary>
/// Initializes this <see cref="Animatable"/> object.
/// </summary>
public Animatable()
{
Transitions = new Transitions();
AnimatableTimer = Timing.AnimationStateTimer
.Select(p =>
{
if (this._playState == PlayState.Pause)
{
return PlayState.Pause;
}
else return p;
})
.Publish()
.RefCount();
}
/// <summary>
/// The specific animations timer for this control.
/// </summary>
/// <returns></returns>
public IObservable<PlayState> AnimatableTimer;
/// <summary> /// <summary>
/// Defines the <see cref="PlayState"/> property. /// Defines the <see cref="PlayState"/> property.
/// </summary> /// </summary>
@ -59,27 +34,25 @@ namespace Avalonia.Animation
{ {
get { return _playState; } get { return _playState; }
set { SetAndRaise(PlayStateProperty, ref _playState, value); } set { SetAndRaise(PlayStateProperty, ref _playState, value); }
} }
/// <summary> /// <summary>
/// Defines the <see cref="Transitions"/> property. /// Defines the <see cref="Transitions"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<Animatable, IEnumerable<ITransition>> TransitionsProperty = public static readonly DirectProperty<Animatable, Transitions> TransitionsProperty =
AvaloniaProperty.RegisterDirect<Animatable, IEnumerable<ITransition>>( AvaloniaProperty.RegisterDirect<Animatable, Transitions>(
nameof(Transitions), nameof(Transitions),
o => o.Transitions, o => o.Transitions,
(o, v) => o.Transitions = v); (o, v) => o.Transitions = v);
private IEnumerable<ITransition> _transitions = new AvaloniaList<ITransition>(); private Transitions _transitions;
/// <summary> /// <summary>
/// Gets or sets the property transitions for the control. /// Gets or sets the property transitions for the control.
/// </summary> /// </summary>
public IEnumerable<ITransition> Transitions public Transitions Transitions
{ {
get { return _transitions; } get { return _transitions ?? (_transitions = new Transitions()); }
set { SetAndRaise(TransitionsProperty, ref _transitions, value); } set { SetAndRaise(TransitionsProperty, ref _transitions, value); }
} }
@ -100,6 +73,5 @@ namespace Avalonia.Animation
} }
} }
} }
} }
} }

86
src/Avalonia.Animation/Animation.cs

@ -17,6 +17,60 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class Animation : AvaloniaList<KeyFrame>, IAnimation public class Animation : AvaloniaList<KeyFrame>, IAnimation
{ {
/// <summary>
/// Gets or sets the animation play state for all animations
/// </summary>
public static PlayState GlobalPlayState { get; set; } = PlayState.Run;
/// <summary>
/// Gets or sets the active time of this animation.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets the repeat count for this animation.
/// </summary>
public RepeatCount RepeatCount { get; set; }
/// <summary>
/// Gets or sets the playback direction for this animation.
/// </summary>
public PlaybackDirection PlaybackDirection { get; set; }
/// <summary>
/// Gets or sets the value fill mode for this animation.
/// </summary>
public FillMode FillMode { get; set; }
/// <summary>
/// Gets or sets the easing function to be used for this animation.
/// </summary>
public Easing Easing { get; set; } = new LinearEasing();
/// <summary>
/// Gets or sets the speed multiple for this animation.
/// </summary>
public double SpeedRatio { get; set; } = 1d;
/// <summary>
/// Gets or sets the delay time for this animation.
/// </summary>
/// <remarks>
/// Describes a delay to be added before the animation starts, and optionally between
/// repeats of the animation if <see cref="DelayBetweenIterations"/> is set.
/// </remarks>
public TimeSpan Delay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether <see cref="Delay"/> will be applied between
/// iterations of the animation.
/// </summary>
/// <remarks>
/// If this property is not set, then <see cref="Delay"/> will only be applied to the first
/// iteration of the animation.
/// </remarks>
public bool DelayBetweenIterations { get; set; }
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)> private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{ {
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ) ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) )
@ -40,38 +94,6 @@ namespace Avalonia.Animation
return null; return null;
} }
public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
/// <summary>
/// Run time of this animation.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Delay time for this animation.
/// </summary>
public TimeSpan Delay { get; set; }
/// <summary>
/// The repeat count for this animation.
/// </summary>
public RepeatCount RepeatCount { get; set; }
/// <summary>
/// The playback direction for this animation.
/// </summary>
public PlaybackDirection PlaybackDirection { get; set; }
/// <summary>
/// The value fill mode for this animation.
/// </summary>
public FillMode FillMode { get; set; }
/// <summary>
/// Easing function to be used.
/// </summary>
public Easing Easing { get; set; } = new LinearEasing();
private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control) private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{ {
var handlerList = new List<(Type type, AvaloniaProperty property)>(); var handlerList = new List<(Type type, AvaloniaProperty property)>();

228
src/Avalonia.Animation/AnimationInstance`1.cs

@ -0,0 +1,228 @@
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
/// <summary>
/// Handles interpolatoin and time-related functions
/// for keyframe animations.
/// </summary>
internal class AnimationInstance<T> : SingleSubscriberObservableBase<T>
{
private T _lastInterpValue;
private T _firstKFValue;
private long _repeatCount;
private double _currentIteration;
private bool _isLooping;
private bool _gotFirstKFValue;
private bool _gotFirstFrameCount;
private bool _iterationDelay;
private FillMode _fillMode;
private PlaybackDirection _animationDirection;
private Animator<T> _parent;
private Animatable _targetControl;
private T _neutralValue;
private double _speedRatio;
private TimeSpan _delay;
private TimeSpan _duration;
private TimeSpan _firstFrameCount;
private TimeSpan _internalClock;
private TimeSpan? _previousClock;
private Easings.Easing _easeFunc;
private Action _onCompleteAction;
private Func<double, T, T> _interpolator;
private IDisposable _timerSubscription;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, Action OnComplete, Func<double, T, T> Interpolator)
{
if (animation.SpeedRatio <= 0)
throw new InvalidOperationException("Speed ratio cannot be negative or zero.");
if (animation.Duration.TotalSeconds <= 0)
throw new InvalidOperationException("Duration cannot be negative or zero.");
_parent = animator;
_easeFunc = animation.Easing;
_targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property);
_speedRatio = animation.SpeedRatio;
_delay = animation.Delay;
_duration = animation.Duration;
_iterationDelay = animation.DelayBetweenIterations;
switch (animation.RepeatCount.RepeatType)
{
case RepeatType.None:
_repeatCount = 1;
break;
case RepeatType.Loop:
_isLooping = true;
break;
case RepeatType.Repeat:
_repeatCount = (long)animation.RepeatCount.Value;
break;
}
_animationDirection = animation.PlaybackDirection;
_fillMode = animation.FillMode;
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
}
protected override void Unsubscribed()
{
_timerSubscription?.Dispose();
}
protected override void Subscribed()
{
_timerSubscription = Timing.AnimationsTimer
.Subscribe(p => this.Step(p));
}
public void Step(TimeSpan frameTick)
{
try
{
InternalStep(frameTick);
}
catch (Exception e)
{
PublishError(e);
}
}
private void DoComplete()
{
if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both)
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
_onCompleteAction?.Invoke();
PublishCompleted();
}
private void DoDelay()
{
if (_fillMode == FillMode.Backward || _fillMode == FillMode.Both)
if (_currentIteration == 0)
PublishNext(_firstKFValue);
else
PublishNext(_lastInterpValue);
}
private void DoPlayStatesAndTime(TimeSpan systemTime)
{
if (Animation.GlobalPlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop)
DoComplete();
if (!_previousClock.HasValue)
{
_previousClock = systemTime;
_internalClock = TimeSpan.Zero;
}
else
{
if (Animation.GlobalPlayState == PlayState.Pause || _targetControl.PlayState == PlayState.Pause)
{
_previousClock = systemTime;
return;
}
var delta = systemTime - _previousClock;
_internalClock += delta.Value;
_previousClock = systemTime;
}
if (!_gotFirstKFValue)
{
_firstKFValue = (T)_parent.First().Value;
_gotFirstKFValue = true;
}
if (!_gotFirstFrameCount)
{
_firstFrameCount = _internalClock;
_gotFirstFrameCount = true;
}
}
private void InternalStep(TimeSpan systemTime)
{
DoPlayStatesAndTime(systemTime);
var time = _internalClock - _firstFrameCount;
var delayEndpoint = _delay;
var iterationEndpoint = delayEndpoint + _duration;
//determine if time is currently in the first iteration.
if (time >= TimeSpan.Zero & time <= iterationEndpoint)
{
_currentIteration = 1;
}
else if (time > iterationEndpoint)
{
//Subtract first iteration to properly get the subsequent iteration time
time -= iterationEndpoint;
if (!_iterationDelay & delayEndpoint > TimeSpan.Zero)
{
delayEndpoint = TimeSpan.Zero;
iterationEndpoint = _duration;
}
//Calculate the current iteration number
_currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2;
}
else
{
_previousClock = systemTime;
return;
}
time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks);
if (!_isLooping)
{
if (_currentIteration > _repeatCount)
DoComplete();
if (time > iterationEndpoint)
DoComplete();
}
// Determine if the current iteration should have its normalized time inverted.
bool isCurIterReverse = _animationDirection == PlaybackDirection.Normal ? false :
_animationDirection == PlaybackDirection.Alternate ? (_currentIteration % 2 == 0) ? false : true :
_animationDirection == PlaybackDirection.AlternateReverse ? (_currentIteration % 2 == 0) ? true : false :
_animationDirection == PlaybackDirection.Reverse ? true : false;
if (delayEndpoint > TimeSpan.Zero & time < delayEndpoint)
{
DoDelay();
}
else
{
// Offset the delay time
time -= delayEndpoint;
iterationEndpoint -= delayEndpoint;
// Normalize time
var interpVal = (double)time.Ticks / iterationEndpoint.Ticks;
if (isCurIterReverse)
interpVal = 1 - interpVal;
// Ease and interpolate
var easedTime = _easeFunc.Ease(interpVal);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
PublishNext(_lastInterpValue);
}
}
}
}

1
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -25,6 +25,7 @@ namespace Avalonia.Animation
Cue = cue; Cue = cue;
} }
internal bool isNeutral;
public Type AnimatorType { get; } public Type AnimatorType { get; }
public Cue Cue { get; } public Cue Cue { get; }
public AvaloniaProperty Property { get; private set; } public AvaloniaProperty Property { get; private set; }

273
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@ -1,273 +0,0 @@
using System;
using System.Linq;
using Avalonia.Data;
namespace Avalonia.Animation
{
/// <summary>
/// Provides statefulness for an iteration of a keyframe animation.
/// </summary>
internal class AnimatorStateMachine<T> : IObservable<object>, IDisposable
{
object _lastInterpValue;
object _firstKFValue;
private ulong _delayTotalFrameCount;
private ulong _durationTotalFrameCount;
private ulong _delayFrameCount;
private ulong _durationFrameCount;
private ulong _repeatCount;
private ulong _currentIteration;
private bool _isLooping;
private bool _isRepeating;
private bool _isReversed;
private bool _checkLoopAndRepeat;
private bool _gotFirstKFValue;
private FillMode _fillMode;
private PlaybackDirection _animationDirection;
private KeyFramesStates _currentState;
private KeyFramesStates _savedState;
private Animator<T> _parent;
private Animation _targetAnimation;
private Animatable _targetControl;
private T _neutralValue;
internal bool _unsubscribe = false;
private IObserver<object> _targetObserver;
private readonly Action _onComplete;
[Flags]
private enum KeyFramesStates
{
Initialize,
DoDelay,
DoRun,
RunForwards,
RunBackwards,
RunApplyValue,
RunComplete,
Pause,
Stop,
Disposed
}
public AnimatorStateMachine(Animation animation, Animatable control, Animator<T> animator, Action onComplete)
{
_parent = animator;
_targetAnimation = animation;
_targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property);
_delayTotalFrameCount = (ulong)(animation.Delay.Ticks / Timing.FrameTick.Ticks);
_durationTotalFrameCount = (ulong)(animation.Duration.Ticks / Timing.FrameTick.Ticks);
switch (animation.RepeatCount.RepeatType)
{
case RepeatType.Loop:
_isLooping = true;
_checkLoopAndRepeat = true;
break;
case RepeatType.Repeat:
_isRepeating = true;
_checkLoopAndRepeat = true;
_repeatCount = animation.RepeatCount.Value;
break;
}
_isReversed = (animation.PlaybackDirection & PlaybackDirection.Reverse) != 0;
_animationDirection = _targetAnimation.PlaybackDirection;
_fillMode = _targetAnimation.FillMode;
if (_durationTotalFrameCount > 0)
_currentState = KeyFramesStates.DoDelay;
else
_currentState = KeyFramesStates.DoRun;
_onComplete = onComplete;
}
public void Step(PlayState _playState, Func<double, T, T> Interpolator)
{
try
{
InternalStep(_playState, Interpolator);
}
catch (Exception e)
{
_targetObserver?.OnError(e);
}
}
private void InternalStep(PlayState _playState, Func<double, T, T> Interpolator)
{
if (!_gotFirstKFValue)
{
_firstKFValue = _parent.First().Value;
_gotFirstKFValue = true;
}
if (_currentState == KeyFramesStates.Disposed)
throw new InvalidProgramException("This KeyFrames Animation is already disposed.");
if (_playState == PlayState.Stop)
_currentState = KeyFramesStates.Stop;
// Save state and pause the machine
if (_playState == PlayState.Pause && _currentState != KeyFramesStates.Pause)
{
_savedState = _currentState;
_currentState = KeyFramesStates.Pause;
}
// Resume the previous state
if (_playState != PlayState.Pause && _currentState == KeyFramesStates.Pause)
_currentState = _savedState;
double _tempDuration = 0d, _easedTime;
bool handled = false;
while (!handled)
{
switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
}
else
{
handled = true;
_delayFrameCount++;
}
break;
case KeyFramesStates.DoRun:
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
break;
case KeyFramesStates.RunForwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
}
break;
case KeyFramesStates.RunBackwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
}
break;
case KeyFramesStates.RunApplyValue:
_easedTime = _targetAnimation.Easing.Ease(_tempDuration);
_durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
handled = true;
break;
case KeyFramesStates.RunComplete:
if (_checkLoopAndRepeat)
{
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
{
if (_currentIteration >= _repeatCount)
{
_currentState = KeyFramesStates.Stop;
}
else
{
_currentState = KeyFramesStates.DoRun;
}
_currentIteration++;
}
if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed;
break;
}
_currentState = KeyFramesStates.Stop;
break;
case KeyFramesStates.Stop:
if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
_targetObserver.OnCompleted();
_onComplete?.Invoke();
Dispose();
handled = true;
break;
default:
handled = true;
break;
}
}
}
public IDisposable Subscribe(IObserver<object> observer)
{
_targetObserver = observer;
return this;
}
public void Dispose()
{
_unsubscribe = true;
_currentState = KeyFramesStates.Disposed;
}
}
}

90
src/Avalonia.Animation/Animator`1.cs

@ -16,8 +16,8 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// List of type-converted keyframes. /// List of type-converted keyframes.
/// </summary> /// </summary>
private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>(); private readonly List<AnimatorKeyFrame> _convertedKeyframes = new List<AnimatorKeyFrame>();
private bool _isVerifiedAndConverted; private bool _isVerifiedAndConverted;
/// <summary> /// <summary>
@ -28,18 +28,17 @@ namespace Avalonia.Animation
public Animator() public Animator()
{ {
// Invalidate keyframes when changed. // Invalidate keyframes when changed.
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; }; this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete) public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> match, Action onComplete)
{ {
if (!_isVerifiedAndConverted) if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames(); VerifyConvertKeyFrames();
return obsMatch return match
// Ignore triggers when global timers are paused. .Where(p => p)
.Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ => .Subscribe(_ =>
{ {
var timerObs = RunKeyFrames(animation, control, onComplete); var timerObs = RunKeyFrames(animation, control, onComplete);
@ -56,53 +55,56 @@ namespace Avalonia.Animation
/// <param name="t">The time parameter, relative to the total animation time</param> /// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t) protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{ {
KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue; AnimatorKeyFrame firstCue, lastCue ;
int kvCount = _convertedKeyframes.Count; int kvCount = _convertedKeyframes.Count;
if (kvCount > 2) if (kvCount > 2)
{ {
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) if (t <= 0.0)
{ {
firstCue = _convertedKeyframes.First(); firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes.Skip(1).First(); lastCue = _convertedKeyframes[1];
} }
else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0) else if (t >= 1.0)
{ {
firstCue = _convertedKeyframes.Skip(kvCount - 2).First(); firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastCue = _convertedKeyframes.Last(); lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1];
} }
else else
{ {
firstCue = _convertedKeyframes.Last(j => j.Key <= t); (double time, int index) maxval = (0.0d, 0);
lastCue = _convertedKeyframes.First(j => j.Key >= t); for (int i = 0; i < _convertedKeyframes.Count; i++)
{
var comp = _convertedKeyframes[i].Cue.CueValue;
if (t >= comp)
{
maxval = (comp, i);
}
}
firstCue = _convertedKeyframes[maxval.index];
lastCue = _convertedKeyframes[maxval.index + 1];
} }
} }
else else
{ {
firstCue = _convertedKeyframes.First(); firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes.Last(); lastCue = _convertedKeyframes[1];
} }
double t0 = firstCue.Key; double t0 = firstCue.Cue.CueValue;
double t1 = lastCue.Key; double t1 = lastCue.Cue.CueValue;
var intraframeTime = (t - t0) / (t1 - t0); var intraframeTime = (t - t0) / (t1 - t0);
var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral); var firstFrameData = (firstCue.GetTypedValue<T>(), firstCue.isNeutral);
var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral); var lastFrameData = (lastCue.GetTypedValue<T>(), lastCue.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData)); return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
} }
/// <summary> /// <summary>
/// Runs the KeyFrames Animation. /// Runs the KeyFrames Animation.
/// </summary> /// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete)
{ {
var stateMachine = new AnimatorStateMachine<T>(animation, control, this, onComplete); var instance = new AnimationInstance<T>(animation, control, this, onComplete, DoInterpolation);
return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
Timing.AnimationStateTimer
.TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => stateMachine.Step(p, DoInterpolation));
return control.Bind(Property, stateMachine, BindingPriority.Animation);
} }
/// <summary> /// <summary>
@ -111,18 +113,26 @@ namespace Avalonia.Animation
protected abstract T DoInterpolation(double time, T neutralValue); protected abstract T DoInterpolation(double time, T neutralValue);
/// <summary> /// <summary>
/// Verifies and converts keyframe values according to this class's target type. /// Verifies, converts and sorts keyframe values according to this class's target type.
/// </summary> /// </summary>
private void VerifyConvertKeyFrames() private void VerifyConvertKeyFrames()
{ {
foreach (AnimatorKeyFrame keyframe in this) foreach (AnimatorKeyFrame keyframe in this)
{ {
_convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false)); _convertedKeyframes.Add(keyframe);
} }
AddNeutralKeyFramesIfNeeded(); AddNeutralKeyFramesIfNeeded();
_isVerifiedAndConverted = true;
var copy = _convertedKeyframes.ToList().OrderBy(p => p.Cue.CueValue);
_convertedKeyframes.Clear();
foreach (AnimatorKeyFrame keyframe in copy)
{
_convertedKeyframes.Add(keyframe);
}
_isVerifiedAndConverted = true;
} }
private void AddNeutralKeyFramesIfNeeded() private void AddNeutralKeyFramesIfNeeded()
@ -130,14 +140,14 @@ namespace Avalonia.Animation
bool hasStartKey, hasEndKey; bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false; hasStartKey = hasEndKey = false;
// Make start and end keyframe mandatory. // Check if there's start and end keyframes.
foreach (var converted in _convertedKeyframes.Keys) foreach (var frame in _convertedKeyframes)
{ {
if (DoubleUtils.AboutEqual(converted, 0.0)) if (frame.Cue.CueValue == 0.0d)
{ {
hasStartKey = true; hasStartKey = true;
} }
else if (DoubleUtils.AboutEqual(converted, 1.0)) else if (frame.Cue.CueValue == 1.0d)
{ {
hasEndKey = true; hasEndKey = true;
} }
@ -151,12 +161,12 @@ namespace Avalonia.Animation
{ {
if (!hasStartKey) if (!hasStartKey)
{ {
_convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
} }
if (!hasEndKey) if (!hasEndKey)
{ {
_convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
} }
} }
} }

5
src/Avalonia.Animation/Cue.cs

@ -5,7 +5,7 @@ using System.Globalization;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// A Cue object for <see cref="KeyFrame"/>. /// Determines the time index for a <see cref="KeyFrame"/>.
/// </summary> /// </summary>
[TypeConverter(typeof(CueTypeConverter))] [TypeConverter(typeof(CueTypeConverter))]
public readonly struct Cue : IEquatable<Cue>, IEquatable<double> public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
@ -82,5 +82,4 @@ namespace Avalonia.Animation
return Cue.Parse((string)value, culture); return Cue.Parse((string)value, culture);
} }
} }
}
}

79
src/Avalonia.Animation/Timing.cs

@ -13,9 +13,6 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public static class Timing public static class Timing
{ {
static ulong _transitionsFrameCount;
static PlayState _globalState = PlayState.Run;
/// <summary> /// <summary>
/// The number of frames per second. /// The number of frames per second.
/// </summary> /// </summary>
@ -30,41 +27,16 @@ namespace Avalonia.Animation
/// Initializes static members of the <see cref="Timing"/> class. /// Initializes static members of the <see cref="Timing"/> class.
/// </summary> /// </summary>
static Timing() static Timing()
{ {
var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance);
AnimationStateTimer = globalTimer AnimationsTimer = globalTimer
.Select(_ => .Select(_ => GetTickCount())
{
return _globalState;
})
.Publish() .Publish()
.RefCount(); .RefCount();
TransitionsTimer = globalTimer
.Select(p => _transitionsFrameCount++)
.Publish()
.RefCount();
}
/// <summary>
/// Sets the animation play state for all animations
/// </summary>
public static void SetGlobalPlayState(PlayState playState)
{
Dispatcher.UIThread.VerifyAccess();
_globalState = playState;
} }
/// <summary> internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount);
/// Gets the animation play state for all animations
/// </summary>
public static PlayState GetGlobalPlayState()
{
Dispatcher.UIThread.VerifyAccess();
return _globalState;
}
/// <summary> /// <summary>
/// Gets the animation timer. /// Gets the animation timer.
@ -74,48 +46,9 @@ namespace Avalonia.Animation
/// defined in <see cref="FramesPerSecond"/>. /// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the current playstate of the animation. /// The parameter passed to a subsciber is the current playstate of the animation.
/// </remarks> /// </remarks>
internal static IObservable<PlayState> AnimationStateTimer internal static IObservable<TimeSpan> AnimationsTimer
{ {
get; get;
} }
/// <summary>
/// Gets the transitions timer.
/// </summary>
/// <remarks>
/// The transitions timer increments usually 60 times per second as
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the number of frames since the animation system was
/// initialized.
/// </remarks>
public static IObservable<ulong> TransitionsTimer
{
get;
}
/// <summary>
/// Gets a timer that fires every frame for the specified duration with delay.
/// </summary>
/// <returns>
/// An observable that notifies the subscriber of the progress along the transition.
/// </returns>
/// <remarks>
/// The parameter passed to the subscriber is the progress along the transition, with
/// 0 being the start and 1 being the end. The observable is guaranteed to fire 0
/// immediately on subscribe and 1 at the end of the duration.
/// </remarks>
public static IObservable<double> GetTransitionsTimer(Animatable control, TimeSpan duration, TimeSpan delay = default(TimeSpan))
{
var startTime = _transitionsFrameCount;
var _duration = (ulong)(duration.Ticks / FrameTick.Ticks);
var endTime = startTime + _duration;
return TransitionsTimer
.TakeWhile(x => x < endTime)
.Select(x => (double)(x - startTime) / _duration)
.StartWith(0.0)
.Concat(Observable.Return(1.0));
}
} }
} }

54
src/Avalonia.Animation/TransitionInstance.cs

@ -0,0 +1,54 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
/// <summary>
/// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>
{
private IDisposable timerSubscription;
private TimeSpan startTime;
private TimeSpan duration;
public TransitionInstance(TimeSpan Duration)
{
duration = Duration;
}
private void TimerTick(TimeSpan t)
{
var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks;
if (interpVal > 1d
|| interpVal < 0d)
{
PublishCompleted();
return;
}
PublishNext(interpVal);
}
protected override void Unsubscribed()
{
timerSubscription?.Dispose();
}
protected override void Subscribed()
{
startTime = Timing.GetTickCount();
timerSubscription = Timing.AnimationsTimer
.Subscribe(t => TimerTick(t));
PublishNext(0.0d);
}
}
}

19
src/Avalonia.Animation/Transition`1.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -23,17 +24,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Gets the easing class to be used. /// Gets the easing class to be used.
/// </summary> /// </summary>
public Easing Easing public Easing Easing { get; set; } = new LinearEasing();
{
get
{
return _easing ?? (_easing = new LinearEasing());
}
set
{
_easing = value;
}
}
/// <inheritdocs/> /// <inheritdocs/>
public AvaloniaProperty Property public AvaloniaProperty Property
@ -60,8 +51,10 @@ namespace Avalonia.Animation
/// <inheritdocs/> /// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, object oldValue, object newValue) public virtual IDisposable Apply(Animatable control, object oldValue, object newValue)
{ {
var transition = DoTransition(Timing.GetTransitionsTimer(control, Duration, TimeSpan.Zero), (T)oldValue, (T)newValue).Select(p => (object)p); var transition = DoTransition(new TransitionInstance(Duration), (T)oldValue, (T)newValue);
return control.Bind(Property, transition, Data.BindingPriority.Animation); return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
} }
} }
} }

16
src/Avalonia.Animation/Utils/DoubleUtils.cs

@ -1,16 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Utils
{
internal static class DoubleUtils
{
internal static bool AboutEqual(double x, double y)
{
double epsilon = Math.Max(Math.Abs(x), Math.Abs(y)) * 1E-15;
return Math.Abs(x - y) <= epsilon;
}
}
}

2
src/Avalonia.Animation/Utils/EasingUtils.cs

@ -13,6 +13,6 @@ namespace Avalonia.Animation.Utils
/// <summary> /// <summary>
/// Half of <see cref="Math.PI"/> /// Half of <see cref="Math.PI"/>
/// </summary> /// </summary>
internal static double HALFPI = Math.PI / 2d; internal const double HALFPI = Math.PI / 2d;
} }
} }

4
src/Avalonia.Controls/Border.cs

@ -43,8 +43,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static Border() static Border()
{ {
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); AffectsRender<Border>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure(BorderThicknessProperty); AffectsMeasure<Border>(BorderThicknessProperty);
} }
/// <summary> /// <summary>

2
src/Avalonia.Controls/Button.cs

@ -80,7 +80,7 @@ namespace Avalonia.Controls
FocusableProperty.OverrideDefaultValue(typeof(Button), true); FocusableProperty.OverrideDefaultValue(typeof(Button), true);
CommandProperty.Changed.Subscribe(CommandChanged); CommandProperty.Changed.Subscribe(CommandChanged);
IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
PseudoClass(IsPressedProperty, ":pressed"); PseudoClass<Button>(IsPressedProperty, ":pressed");
} }
/// <summary> /// <summary>

6
src/Avalonia.Controls/ButtonSpinner.cs

@ -85,8 +85,8 @@ namespace Avalonia.Controls
static ButtonSpinner() static ButtonSpinner()
{ {
AllowSpinProperty.Changed.Subscribe(AllowSpinChanged); AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left"); PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right"); PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
} }
/// <summary> /// <summary>
@ -260,4 +260,4 @@ namespace Avalonia.Controls
} }
} }
} }
} }

26
src/Avalonia.Controls/Canvas.cs

@ -48,7 +48,7 @@ namespace Avalonia.Controls
static Canvas() static Canvas()
{ {
ClipToBoundsProperty.OverrideDefaultValue<Canvas>(false); ClipToBoundsProperty.OverrideDefaultValue<Canvas>(false);
AffectsCanvasArrange(LeftProperty, TopProperty, RightProperty, BottomProperty); AffectsParentArrange<Canvas>(LeftProperty, TopProperty, RightProperty, BottomProperty);
} }
/// <summary> /// <summary>
@ -207,29 +207,5 @@ namespace Avalonia.Controls
return finalSize; return finalSize;
} }
/// <summary>
/// Marks a property on a child as affecting the canvas' arrangement.
/// </summary>
/// <param name="properties">The properties.</param>
private static void AffectsCanvasArrange(params AvaloniaProperty[] properties)
{
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsCanvasArrangeInvalidate);
}
}
/// <summary>
/// Calls <see cref="Layoutable.InvalidateArrange"/> on the parent of the control whose
/// property changed, if that parent is a canvas.
/// </summary>
/// <param name="e">The event args.</param>
private static void AffectsCanvasArrangeInvalidate(AvaloniaPropertyChangedEventArgs e)
{
var control = e.Sender as IControl;
var canvas = control?.VisualParent as Canvas;
canvas?.InvalidateArrange();
}
} }
} }

4
src/Avalonia.Controls/ContentControl.cs

@ -45,8 +45,8 @@ namespace Avalonia.Controls
static ContentControl() static ContentControl()
{ {
ContentControlMixin.Attach<ContentControl>(ContentProperty, x => x.LogicalChildren); ContentControlMixin.Attach<ContentControl>(ContentProperty, x => x.LogicalChildren);
PseudoClass(ContentProperty, x => x != null, ":valid"); PseudoClass<ContentControl, object>(ContentProperty, x => x != null, ":valid");
PseudoClass(ContentProperty, x => x == null, ":invalid"); PseudoClass<ContentControl, object>(ContentProperty, x => x == null, ":invalid");
} }
/// <summary> /// <summary>

2
src/Avalonia.Controls/Decorator.cs

@ -28,7 +28,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static Decorator() static Decorator()
{ {
AffectsMeasure(ChildProperty, PaddingProperty); AffectsMeasure<Decorator>(ChildProperty, PaddingProperty);
ChildProperty.Changed.AddClassHandler<Decorator>(x => x.ChildChanged); ChildProperty.Changed.AddClassHandler<Decorator>(x => x.ChildChanged);
} }

4
src/Avalonia.Controls/DockPanel.cs

@ -37,7 +37,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static DockPanel() static DockPanel()
{ {
AffectsArrange(DockProperty); AffectsParentMeasure<DockPanel>(DockProperty);
} }
/// <summary> /// <summary>
@ -173,4 +173,4 @@ namespace Avalonia.Controls
return arrangeSize; return arrangeSize;
} }
} }
} }

6
src/Avalonia.Controls/DrawingPresenter.cs

@ -8,8 +8,8 @@ namespace Avalonia.Controls
{ {
static DrawingPresenter() static DrawingPresenter()
{ {
AffectsMeasure(DrawingProperty); AffectsMeasure<DrawingPresenter>(DrawingProperty);
AffectsRender(DrawingProperty); AffectsRender<DrawingPresenter>(DrawingProperty);
} }
public static readonly StyledProperty<Drawing> DrawingProperty = public static readonly StyledProperty<Drawing> DrawingProperty =
@ -56,4 +56,4 @@ namespace Avalonia.Controls
} }
} }
} }
} }

10
src/Avalonia.Controls/Expander.cs

@ -35,12 +35,12 @@ namespace Avalonia.Controls
static Expander() static Expander()
{ {
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down"); PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up"); PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left"); PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right"); PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right");
PseudoClass(IsExpandedProperty, ":expanded"); PseudoClass<Expander>(IsExpandedProperty, ":expanded");
IsExpandedProperty.Changed.AddClassHandler<Expander>(x => x.OnIsExpandedChanged); IsExpandedProperty.Changed.AddClassHandler<Expander>(x => x.OnIsExpandedChanged);
} }

5
src/Avalonia.Controls/Grid.cs

@ -48,6 +48,11 @@ namespace Avalonia.Controls
private RowDefinitions _rowDefinitions; private RowDefinitions _rowDefinitions;
static Grid()
{
AffectsParentMeasure<Grid>(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty);
}
/// <summary> /// <summary>
/// Gets or sets the columns definitions for the grid. /// Gets or sets the columns definitions for the grid.
/// </summary> /// </summary>

3
src/Avalonia.Controls/Image.cs

@ -25,8 +25,7 @@ namespace Avalonia.Controls
static Image() static Image()
{ {
AffectsRender(SourceProperty); AffectsRender<Image>(SourceProperty, StretchProperty);
AffectsRender(StretchProperty);
} }
/// <summary> /// <summary>

42
src/Avalonia.Controls/Panel.cs

@ -72,6 +72,32 @@ namespace Avalonia.Controls
base.Render(context); base.Render(context);
} }
/// <summary>
/// Marks a property on a child as affecting the parent panel's arrangement.
/// </summary>
/// <param name="properties">The properties.</param>
protected static void AffectsParentArrange<TPanel>(params AvaloniaProperty[] properties)
where TPanel : class, IPanel
{
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsParentArrangeInvalidate<TPanel>);
}
}
/// <summary>
/// Marks a property on a child as affecting the parent panel's measurement.
/// </summary>
/// <param name="properties">The properties.</param>
protected static void AffectsParentMeasure<TPanel>(params AvaloniaProperty[] properties)
where TPanel : class, IPanel
{
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsParentMeasureInvalidate<TPanel>);
}
}
/// <summary> /// <summary>
/// Called when the <see cref="Children"/> collection changes. /// Called when the <see cref="Children"/> collection changes.
/// </summary> /// </summary>
@ -116,5 +142,21 @@ namespace Avalonia.Controls
InvalidateMeasure(); InvalidateMeasure();
} }
private static void AffectsParentArrangeInvalidate<TPanel>(AvaloniaPropertyChangedEventArgs e)
where TPanel : class, IPanel
{
var control = e.Sender as IControl;
var panel = control?.VisualParent as TPanel;
panel?.InvalidateArrange();
}
private static void AffectsParentMeasureInvalidate<TPanel>(AvaloniaPropertyChangedEventArgs e)
where TPanel : class, IPanel
{
var control = e.Sender as IControl;
var panel = control?.VisualParent as TPanel;
panel?.InvalidateMeasure();
}
} }
} }

4
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -90,8 +90,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
static ContentPresenter() static ContentPresenter()
{ {
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); AffectsRender<ContentPresenter>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure(BorderThicknessProperty, PaddingProperty); AffectsMeasure<ContentPresenter>(BorderThicknessProperty, PaddingProperty);
ContentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged); ContentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged); ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.TemplatedParentChanged); TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.TemplatedParentChanged);

2
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -279,6 +279,6 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Invalidates the current scroll. /// Invalidates the current scroll.
/// </summary> /// </summary>
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll(); protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll?.Invoke();
} }
} }

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -72,7 +72,7 @@ namespace Avalonia.Controls.Presenters
{ {
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>(x => x.ChildChanged); ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>(x => x.ChildChanged);
AffectsArrange(OffsetProperty); AffectsArrange<ScrollContentPresenter>(OffsetProperty);
} }
/// <summary> /// <summary>

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

@ -38,7 +38,7 @@ namespace Avalonia.Controls.Presenters
static TextPresenter() static TextPresenter()
{ {
AffectsRender(PasswordCharProperty); AffectsRender<TextPresenter>(PasswordCharProperty);
} }
public TextPresenter() public TextPresenter()

2
src/Avalonia.Controls/Primitives/AccessText.cs

@ -28,7 +28,7 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
static AccessText() static AccessText()
{ {
AffectsRender(ShowAccessKeyProperty); AffectsRender<AccessText>(ShowAccessKeyProperty);
} }
/// <summary> /// <summary>

4
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -54,8 +54,8 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
static ScrollBar() static ScrollBar()
{ {
PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>(o => o.OnThumbDragDelta, RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>(o => o.OnThumbDragDelta, RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>(o => o.OnThumbDragComplete, RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>(o => o.OnThumbDragComplete, RoutingStrategies.Bubble);

6
src/Avalonia.Controls/Primitives/ToggleButton.cs

@ -24,9 +24,9 @@ namespace Avalonia.Controls.Primitives
static ToggleButton() static ToggleButton()
{ {
PseudoClass(IsCheckedProperty, c => c == true, ":checked"); PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == true, ":checked");
PseudoClass(IsCheckedProperty, c => c == false, ":unchecked"); PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == false, ":unchecked");
PseudoClass(IsCheckedProperty, c => c == null, ":indeterminate"); PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == null, ":indeterminate");
} }
public bool? IsChecked public bool? IsChecked

4
src/Avalonia.Controls/Primitives/Track.cs

@ -39,10 +39,12 @@ namespace Avalonia.Controls.Primitives
static Track() static Track()
{ {
PseudoClass<Track, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<Track, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
ThumbProperty.Changed.AddClassHandler<Track>(x => x.ThumbChanged); ThumbProperty.Changed.AddClassHandler<Track>(x => x.ThumbChanged);
IncreaseButtonProperty.Changed.AddClassHandler<Track>(x => x.ButtonChanged); IncreaseButtonProperty.Changed.AddClassHandler<Track>(x => x.ButtonChanged);
DecreaseButtonProperty.Changed.AddClassHandler<Track>(x => x.ButtonChanged); DecreaseButtonProperty.Changed.AddClassHandler<Track>(x => x.ButtonChanged);
AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); AffectsArrange<Track>(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
} }
public double Minimum public double Minimum

6
src/Avalonia.Controls/ProgressBar.cs

@ -33,9 +33,9 @@ namespace Avalonia.Controls
static ProgressBar() static ProgressBar()
{ {
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate"); PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged); ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
} }

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

@ -30,11 +30,10 @@ namespace Avalonia.Controls.Shapes
private Geometry _renderedGeometry; private Geometry _renderedGeometry;
bool _calculateTransformOnArrange = false; bool _calculateTransformOnArrange = false;
static Shape() static Shape()
{ {
AffectsMeasure(StretchProperty, StrokeThicknessProperty); AffectsMeasure<Shape>(StretchProperty, StrokeThicknessProperty);
AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty); AffectsRender<Shape>(FillProperty, StrokeProperty, StrokeDashArrayProperty);
} }
public Geometry DefiningGeometry public Geometry DefiningGeometry

2
src/Avalonia.Controls/Slider.cs

@ -42,6 +42,8 @@ namespace Avalonia.Controls
static Slider() static Slider()
{ {
OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
PseudoClass<Slider, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<Slider, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
Thumb.DragStartedEvent.AddClassHandler<Slider>(x => x.OnThumbDragStarted, RoutingStrategies.Bubble); Thumb.DragStartedEvent.AddClassHandler<Slider>(x => x.OnThumbDragStarted, RoutingStrategies.Bubble);
Thumb.DragDeltaEvent.AddClassHandler<Slider>(x => x.OnThumbDragDelta, RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler<Slider>(x => x.OnThumbDragDelta, RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<Slider>(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler<Slider>(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble);

4
src/Avalonia.Controls/StackPanel.cs

@ -29,8 +29,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static StackPanel() static StackPanel()
{ {
AffectsMeasure(SpacingProperty); AffectsMeasure<StackPanel>(SpacingProperty);
AffectsMeasure(OrientationProperty); AffectsMeasure<StackPanel>(OrientationProperty);
} }
/// <summary> /// <summary>

2
src/Avalonia.Controls/TabControl.cs

@ -44,7 +44,7 @@ namespace Avalonia.Controls
{ {
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected); SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue<TabControl>(false); FocusableProperty.OverrideDefaultValue<TabControl>(false);
AffectsMeasure(TabStripPlacementProperty); AffectsMeasure<TabControl>(TabStripPlacementProperty);
} }
/// <summary> /// <summary>

9
src/Avalonia.Controls/TextBlock.cs

@ -99,10 +99,11 @@ namespace Avalonia.Controls
static TextBlock() static TextBlock()
{ {
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true); ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender(ForegroundProperty); AffectsRender<TextBlock>(
AffectsRender(FontWeightProperty); ForegroundProperty,
AffectsRender(FontSizeProperty); FontWeightProperty,
AffectsRender(FontStyleProperty); FontSizeProperty,
FontStyleProperty);
} }
/// <summary> /// <summary>

2
src/Avalonia.Controls/TopLevel.cs

@ -59,7 +59,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static TopLevel() static TopLevel()
{ {
AffectsMeasure(ClientSizeProperty); AffectsMeasure<TopLevel>(ClientSizeProperty);
} }
/// <summary> /// <summary>

2
src/Avalonia.Controls/WrapPanel.cs

@ -30,7 +30,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static WrapPanel() static WrapPanel()
{ {
AffectsMeasure(OrientationProperty); AffectsMeasure<WrapPanel>(OrientationProperty);
} }
/// <summary> /// <summary>

6
src/Avalonia.Input/InputElement.cs

@ -168,9 +168,9 @@ namespace Avalonia.Input
PointerReleasedEvent.AddClassHandler<InputElement>(x => x.OnPointerReleased); PointerReleasedEvent.AddClassHandler<InputElement>(x => x.OnPointerReleased);
PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged); PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged);
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); PseudoClass<InputElement, bool>(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass(IsFocusedProperty, ":focus"); PseudoClass<InputElement>(IsFocusedProperty, ":focus");
PseudoClass(IsPointerOverProperty, ":pointerover"); PseudoClass<InputElement>(IsPointerOverProperty, ":pointerover");
} }
/// <summary> /// <summary>

68
src/Avalonia.Layout/Layoutable.cs

@ -140,7 +140,7 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
static Layoutable() static Layoutable()
{ {
AffectsMeasure( AffectsMeasure<Layoutable>(
IsVisibleProperty, IsVisibleProperty,
WidthProperty, WidthProperty,
HeightProperty, HeightProperty,
@ -427,11 +427,32 @@ namespace Avalonia.Layout
/// After a call to this method in a control's static constructor, any change to the /// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateMeasure"/> to be called on the element. /// property will cause <see cref="InvalidateMeasure"/> to be called on the element.
/// </remarks> /// </remarks>
[Obsolete("Use AffectsMeasure<T> and specify the control type.")]
protected static void AffectsMeasure(params AvaloniaProperty[] properties) protected static void AffectsMeasure(params AvaloniaProperty[] properties)
{ {
AffectsMeasure<Layoutable>(properties);
}
/// <summary>
/// Marks a property as affecting the control's measurement.
/// </summary>
/// <typeparam name="T">The control which the property affects.</typeparam>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateMeasure"/> to be called on the element.
/// </remarks>
protected static void AffectsMeasure<T>(params AvaloniaProperty[] properties)
where T : class, ILayoutable
{
void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
(e.Sender as T)?.InvalidateMeasure();
}
foreach (var property in properties) foreach (var property in properties)
{ {
property.Changed.Subscribe(AffectsMeasureInvalidate); property.Changed.Subscribe(Invalidate);
} }
} }
@ -443,11 +464,32 @@ namespace Avalonia.Layout
/// After a call to this method in a control's static constructor, any change to the /// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateArrange"/> to be called on the element. /// property will cause <see cref="InvalidateArrange"/> to be called on the element.
/// </remarks> /// </remarks>
[Obsolete("Use AffectsArrange<T> and specify the control type.")]
protected static void AffectsArrange(params AvaloniaProperty[] properties) protected static void AffectsArrange(params AvaloniaProperty[] properties)
{ {
AffectsArrange<Layoutable>(properties);
}
/// <summary>
/// Marks a property as affecting the control's arrangement.
/// </summary>
/// <typeparam name="T">The control which the property affects.</typeparam>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateArrange"/> to be called on the element.
/// </remarks>
protected static void AffectsArrange<T>(params AvaloniaProperty[] properties)
where T : class, ILayoutable
{
void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
(e.Sender as T)?.InvalidateArrange();
}
foreach (var property in properties) foreach (var property in properties)
{ {
property.Changed.Subscribe(AffectsArrangeInvalidate); property.Changed.Subscribe(Invalidate);
} }
} }
@ -632,26 +674,6 @@ namespace Avalonia.Layout
base.OnVisualParentChanged(oldParent, newParent); base.OnVisualParentChanged(oldParent, newParent);
} }
/// <summary>
/// Calls <see cref="InvalidateMeasure"/> on the control on which a property changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void AffectsMeasureInvalidate(AvaloniaPropertyChangedEventArgs e)
{
ILayoutable control = e.Sender as ILayoutable;
control?.InvalidateMeasure();
}
/// <summary>
/// Calls <see cref="InvalidateArrange"/> on the control on which a property changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void AffectsArrangeInvalidate(AvaloniaPropertyChangedEventArgs e)
{
ILayoutable control = e.Sender as ILayoutable;
control?.InvalidateArrange();
}
/// <summary> /// <summary>
/// Tests whether any of a <see cref="Rect"/>'s properties include negative values, /// Tests whether any of a <see cref="Rect"/>'s properties include negative values,
/// a NaN or Infinity. /// a NaN or Infinity.

45
src/Avalonia.Styling/StyledElement.cs

@ -493,22 +493,53 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="className">The pseudo-class.</param> /// <param name="className">The pseudo-class.</param>
[Obsolete("Use PseudoClass<TOwner> and specify the control type.")]
protected static void PseudoClass(AvaloniaProperty<bool> property, string className) protected static void PseudoClass(AvaloniaProperty<bool> property, string className)
{ {
PseudoClass(property, x => x, className); PseudoClass<StyledElement>(property, className);
}
/// <summary>
/// Adds a pseudo-class to be set when a property is true.
/// </summary>
/// <typeparam name="TOwner">The type to apply the pseudo-class to.</typeparam>
/// <param name="property">The property.</param>
/// <param name="className">The pseudo-class.</param>
protected static void PseudoClass<TOwner>(AvaloniaProperty<bool> property, string className)
where TOwner : class, IStyledElement
{
PseudoClass<TOwner, bool>(property, x => x, className);
}
/// <summary>
/// Adds a pseudo-class to be set when a property equals a certain value.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="selector">Returns a boolean value based on the property value.</param>
/// <param name="className">The pseudo-class.</param>
[Obsolete("Use PseudoClass<TOwner, TProperty> and specify the control type.")]
protected static void PseudoClass<TProperty>(
AvaloniaProperty<TProperty> property,
Func<TProperty, bool> selector,
string className)
{
PseudoClass<StyledElement, TProperty>(property, selector, className);
} }
/// <summary> /// <summary>
/// Adds a pseudo-class to be set when a property equals a certain value. /// Adds a pseudo-class to be set when a property equals a certain value.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam> /// <typeparam name="TProperty">The type of the property.</typeparam>
/// <typeparam name="TOwner">The type to apply the pseudo-class to.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="selector">Returns a boolean value based on the property value.</param> /// <param name="selector">Returns a boolean value based on the property value.</param>
/// <param name="className">The pseudo-class.</param> /// <param name="className">The pseudo-class.</param>
protected static void PseudoClass<T>( protected static void PseudoClass<TOwner, TProperty>(
AvaloniaProperty<T> property, AvaloniaProperty<TProperty> property,
Func<T, bool> selector, Func<TProperty, bool> selector,
string className) string className)
where TOwner : class, IStyledElement
{ {
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(selector != null); Contract.Requires<ArgumentNullException>(selector != null);
@ -520,10 +551,10 @@ namespace Avalonia
} }
property.Changed.Merge(property.Initialized) property.Changed.Merge(property.Initialized)
.Where(e => e.Sender is StyledElement) .Where(e => e.Sender is TOwner)
.Subscribe(e => .Subscribe(e =>
{ {
if (selector((T)e.NewValue)) if (selector((TProperty)e.NewValue))
{ {
((StyledElement)e.Sender).PseudoClasses.Add(className); ((StyledElement)e.Sender).PseudoClasses.Add(className);
} }

10
src/Avalonia.Themes.Default/CheckBox.xaml

@ -1,12 +1,16 @@
<Styles xmlns="https://github.com/avaloniaui"> <Styles xmlns="https://github.com/avaloniaui">
<Style Selector="CheckBox"> <Style Selector="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Grid ColumnDefinitions="Auto,*" Background="{TemplateBinding Background}"> <Grid ColumnDefinitions="Auto,*">
<Border Name="border" <Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Width="18" Width="18"
@ -31,10 +35,12 @@
</Panel> </Panel>
</Border> </Border>
<ContentPresenter Name="PART_ContentPresenter" <ContentPresenter Name="PART_ContentPresenter"
TextBlock.Foreground="{TemplateBinding Foreground}"
ContentTemplate="{TemplateBinding ContentTemplate}" ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
Margin="4,0,0,0" Margin="4,0,0,0"
VerticalAlignment="Center" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Grid.Column="1"/> Grid.Column="1"/>
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>

8
src/Avalonia.Themes.Default/TreeView.xaml

@ -3,11 +3,15 @@
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="Padding" Value="4"/> <Setter Property="Padding" Value="4"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border BorderBrush="{TemplateBinding BorderBrush}" <Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Background="{TemplateBinding Background}"> <ScrollViewer Background="{TemplateBinding Background}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}" Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
@ -17,4 +21,4 @@
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

8
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -38,11 +38,11 @@ namespace Avalonia.Animation
new Setter new Setter
{ {
Property = Visual.OpacityProperty, Property = Visual.OpacityProperty,
Value = 0.0 Value = 0d
} }
) )
{ {
Cue = new Cue(1.0) Cue = new Cue(1d)
} }
}; };
_fadeInAnimation = new Animation _fadeInAnimation = new Animation
@ -52,11 +52,11 @@ namespace Avalonia.Animation
new Setter new Setter
{ {
Property = Visual.OpacityProperty, Property = Visual.OpacityProperty,
Value = 0.0 Value = 0d
} }
) )
{ {
Cue = new Cue(0.0) Cue = new Cue(0d)
} }
}; };
_fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration; _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration;

14
src/Avalonia.Visuals/Animation/PageSlide.cs

@ -86,11 +86,11 @@ namespace Avalonia.Animation
new Setter new Setter
{ {
Property = translateProperty, Property = translateProperty,
Value = 0 Value = 0d
} }
) )
{ {
Cue = new Cue(0.0) Cue = new Cue(0d)
}, },
new KeyFrame new KeyFrame
( (
@ -101,7 +101,7 @@ namespace Avalonia.Animation
} }
) )
{ {
Cue = new Cue(1.0) Cue = new Cue(1d)
} }
}; };
animation.Duration = Duration; animation.Duration = Duration;
@ -119,22 +119,22 @@ namespace Avalonia.Animation
new Setter new Setter
{ {
Property = translateProperty, Property = translateProperty,
Value = forward ? -distance : distance Value = forward ? distance : -distance
} }
) )
{ {
Cue = new Cue(0.0) Cue = new Cue(0d)
}, },
new KeyFrame new KeyFrame
( (
new Setter new Setter
{ {
Property = translateProperty, Property = translateProperty,
Value = 0 Value = 0d
} }
) )
{ {
Cue = new Cue(1.0) Cue = new Cue(1d)
}, },
}; };
animation.Duration = Duration; animation.Duration = Duration;

11
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@ -30,19 +30,20 @@ namespace Avalonia.Rendering.SceneGraph
Matrix Transform { get; } Matrix Transform { get; }
/// <summary> /// <summary>
/// Gets the bounds for the node's geometry in global coordinates. /// Gets the bounds of the node's geometry in global coordinates.
/// </summary> /// </summary>
Rect Bounds { get; } Rect Bounds { get; }
/// <summary> /// <summary>
/// Gets the clip bounds for the node in global coordinates. /// Gets the clip bounds for the node in global coordinates.
/// </summary> /// </summary>
/// <remarks>
/// This clip does not take into account parent clips, to find the absolute clip bounds
/// it is necessary to traverse the tree.
/// </remarks>
Rect ClipBounds { get; } Rect ClipBounds { get; }
/// <summary>
/// Gets the layout bounds for the node in global coordinates.
/// </summary>
Rect LayoutBounds { get; }
/// <summary> /// <summary>
/// Whether the node is clipped to <see cref="ClipBounds"/>. /// Whether the node is clipped to <see cref="ClipBounds"/>.
/// </summary> /// </summary>

4
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -167,8 +167,9 @@ namespace Avalonia.Rendering.SceneGraph
using (context.PushPostTransform(m)) using (context.PushPostTransform(m))
using (context.PushTransformContainer()) using (context.PushTransformContainer())
{ {
var globalBounds = bounds.TransformToAABB(contextImpl.Transform);
var clipBounds = clipToBounds ? var clipBounds = clipToBounds ?
bounds.TransformToAABB(contextImpl.Transform).Intersect(clip) : globalBounds.Intersect(clip) :
clip; clip;
forceRecurse = forceRecurse || forceRecurse = forceRecurse ||
@ -179,6 +180,7 @@ namespace Avalonia.Rendering.SceneGraph
node.Transform = contextImpl.Transform; node.Transform = contextImpl.Transform;
node.ClipBounds = clipBounds; node.ClipBounds = clipBounds;
node.ClipToBounds = clipToBounds; node.ClipToBounds = clipToBounds;
node.LayoutBounds = globalBounds;
node.GeometryClip = visual.Clip?.PlatformImpl; node.GeometryClip = visual.Clip?.PlatformImpl;
node.Opacity = opacity; node.Opacity = opacity;

5
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -58,6 +58,9 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/> /// <inheritdoc/>
public Rect ClipBounds { get; set; } public Rect ClipBounds { get; set; }
/// <inheritdoc/>
public Rect LayoutBounds { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public bool ClipToBounds { get; set; } public bool ClipToBounds { get; set; }
@ -266,7 +269,7 @@ namespace Avalonia.Rendering.SceneGraph
if (OpacityMask != null) if (OpacityMask != null)
{ {
context.PushOpacityMask(OpacityMask, ClipBounds); context.PushOpacityMask(OpacityMask, LayoutBounds);
} }
} }

36
src/Avalonia.Visuals/Visual.cs

@ -100,7 +100,7 @@ namespace Avalonia
/// </summary> /// </summary>
static Visual() static Visual()
{ {
AffectsRender( AffectsRender<Visual>(
BoundsProperty, BoundsProperty,
ClipProperty, ClipProperty,
ClipToBoundsProperty, ClipToBoundsProperty,
@ -320,11 +320,34 @@ namespace Avalonia
/// on the control which when changed should cause a redraw. This is similar to WPF's /// on the control which when changed should cause a redraw. This is similar to WPF's
/// FrameworkPropertyMetadata.AffectsRender flag. /// FrameworkPropertyMetadata.AffectsRender flag.
/// </remarks> /// </remarks>
[Obsolete("Use AffectsRender<T> and specify the control type.")]
protected static void AffectsRender(params AvaloniaProperty[] properties) protected static void AffectsRender(params AvaloniaProperty[] properties)
{ {
AffectsRender<Visual>(properties);
}
/// <summary>
/// Indicates that a property change should cause <see cref="InvalidateVisual"/> to be
/// called.
/// </summary>
/// <typeparam name="T">The control which the property affects.</typeparam>
/// <param name="properties">The properties.</param>
/// <remarks>
/// This method should be called in a control's static constructor with each property
/// on the control which when changed should cause a redraw. This is similar to WPF's
/// FrameworkPropertyMetadata.AffectsRender flag.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : class, IVisual
{
void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
(e.Sender as T)?.InvalidateVisual();
}
foreach (var property in properties) foreach (var property in properties)
{ {
property.Changed.Subscribe(AffectsRenderInvalidate); property.Changed.Subscribe(Invalidate);
} }
} }
@ -412,15 +435,6 @@ namespace Avalonia
RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue);
} }
/// <summary>
/// Called when a property changes that should invalidate the visual.
/// </summary>
/// <param name="e">The event args.</param>
private static void AffectsRenderInvalidate(AvaloniaPropertyChangedEventArgs e)
{
(e.Sender as Visual)?.InvalidateVisual();
}
/// <summary> /// <summary>
/// Gets the visual offset from the specified ancestor. /// Gets the visual offset from the specified ancestor.
/// </summary> /// </summary>

40
src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using SharpDX; using SharpDX;
using SharpDX.Direct2D1; using SharpDX.Direct2D1;
using SharpDX.DirectWrite; using SharpDX.DirectWrite;
@ -9,7 +8,7 @@ using SharpDX.Mathematics.Interop;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
internal class AvaloniaTextRenderer : TextRenderer internal class AvaloniaTextRenderer : TextRendererBase
{ {
private readonly DrawingContextImpl _context; private readonly DrawingContextImpl _context;
@ -27,18 +26,7 @@ namespace Avalonia.Direct2D1.Media
_foreground = foreground; _foreground = foreground;
} }
public IDisposable Shadow public override Result DrawGlyphRun(
{
get;
set;
}
public void Dispose()
{
Shadow?.Dispose();
}
public Result DrawGlyphRun(
object clientDrawingContext, object clientDrawingContext,
float baselineOriginX, float baselineOriginX,
float baselineOriginY, float baselineOriginY,
@ -68,34 +56,14 @@ namespace Avalonia.Direct2D1.Media
return Result.Ok; return Result.Ok;
} }
public Result DrawInlineObject(object clientDrawingContext, float originX, float originY, InlineObject inlineObject, bool isSideways, bool isRightToLeft, ComObject clientDrawingEffect) public override RawMatrix3x2 GetCurrentTransform(object clientDrawingContext)
{
throw new NotImplementedException();
}
public Result DrawStrikethrough(object clientDrawingContext, float baselineOriginX, float baselineOriginY, ref Strikethrough strikethrough, ComObject clientDrawingEffect)
{
throw new NotImplementedException();
}
public Result DrawUnderline(object clientDrawingContext, float baselineOriginX, float baselineOriginY, ref Underline underline, ComObject clientDrawingEffect)
{
throw new NotImplementedException();
}
public RawMatrix3x2 GetCurrentTransform(object clientDrawingContext)
{ {
return _renderTarget.Transform; return _renderTarget.Transform;
} }
public float GetPixelsPerDip(object clientDrawingContext) public override float GetPixelsPerDip(object clientDrawingContext)
{ {
return _renderTarget.DotsPerInch.Width / 96; return _renderTarget.DotsPerInch.Width / 96;
} }
public bool IsPixelSnappingDisabled(object clientDrawingContext)
{
return false;
}
} }
} }

30
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@ -21,22 +21,21 @@ namespace Avalonia.Direct2D1.Media
{ {
Text = text; Text = text;
var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface); using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface))
textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout(
Direct2D1Platform.DirectWriteFactory,
Text ?? string.Empty,
textFormat,
(float)constraint.Width,
(float)constraint.Height)
{ {
TextAlignment = textAlignment.ToDirect2D() textFormat.WordWrapping =
}; wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
textFormat.Dispose(); TextLayout = new DWrite.TextLayout(
Direct2D1Platform.DirectWriteFactory,
Text ?? string.Empty,
textFormat,
(float)constraint.Width,
(float)constraint.Height)
{
TextAlignment = textAlignment.ToDirect2D()
};
}
if (spans != null) if (spans != null)
{ {
@ -108,6 +107,7 @@ namespace Avalonia.Direct2D1.Media
private Size Measure() private Size Measure()
{ {
var metrics = TextLayout.Metrics; var metrics = TextLayout.Metrics;
var width = metrics.WidthIncludingTrailingWhitespace; var width = metrics.WidthIncludingTrailingWhitespace;
if (float.IsNaN(width)) if (float.IsNaN(width))

13
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs

@ -1,14 +1,13 @@
using System; // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.IO; using System.IO;
using SharpDX.Direct2D1; using SharpDX.WIC;
using Bitmap = SharpDX.Direct2D1.Bitmap;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
using SharpDX.WIC;
using Bitmap = SharpDX.Direct2D1.Bitmap;
using PixelFormat = SharpDX.Direct2D1.PixelFormat;
/// <summary> /// <summary>
/// A Direct2D Bitmap implementation that uses a GPU memory bitmap as its image. /// A Direct2D Bitmap implementation that uses a GPU memory bitmap as its image.
/// </summary> /// </summary>

24
tests/Avalonia.Controls.UnitTests/DockPanelTests.cs

@ -58,5 +58,29 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(50, 350, 500, 50), target.Children[3].Bounds); Assert.Equal(new Rect(50, 350, 500, 50), target.Children[3].Bounds);
Assert.Equal(new Rect(50, 50, 500, 300), target.Children[4].Bounds); Assert.Equal(new Rect(50, 50, 500, 300), target.Children[4].Bounds);
} }
[Fact]
public void Changing_Child_Dock_Invalidates_Measure()
{
Border child;
var target = new DockPanel
{
Children =
{
(child = new Border
{
[DockPanel.DockProperty] = Dock.Left,
}),
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.True(target.IsMeasureValid);
DockPanel.SetDock(child, Dock.Right);
Assert.False(target.IsMeasureValid);
}
} }
} }

26
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -154,5 +154,31 @@ namespace Avalonia.Controls.UnitTests
GridAssert.ChildrenHeight(rowGrid, 200, 300, 300); GridAssert.ChildrenHeight(rowGrid, 200, 300, 300);
GridAssert.ChildrenWidth(columnGrid, 200, 300, 300); GridAssert.ChildrenWidth(columnGrid, 200, 300, 300);
} }
[Fact]
public void Changing_Child_Column_Invalidates_Measure()
{
Border child;
var target = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,*"),
Children =
{
(child = new Border
{
[Grid.ColumnProperty] = 0,
}),
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.True(target.IsMeasureValid);
Grid.SetColumn(child, 1);
Assert.False(target.IsMeasureValid);
}
} }
} }

37
tests/Avalonia.RenderTests/OpacityMaskTests.cs

@ -55,5 +55,42 @@ namespace Avalonia.Direct2D1.RenderTests
await RenderToFile(target); await RenderToFile(target);
CompareImages(); CompareImages();
} }
[Fact]
public async Task RenderTansform_Applies_To_Opacity_Mask()
{
var target = new Canvas
{
OpacityMask = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops = new List<GradientStop>
{
new GradientStop(Color.FromUInt32(0xffffffff), 0),
new GradientStop(Color.FromUInt32(0x00ffffff), 1)
}
},
RenderTransform = new RotateTransform(90),
Width = 76,
Height = 76,
Children =
{
new Path
{
Width = 32,
Height = 40,
[Canvas.LeftProperty] = 23,
[Canvas.TopProperty] = 18,
Stretch = Stretch.Fill,
Fill = Brushes.Red,
Data = StreamGeometry.Parse("F1 M 27,18L 23,26L 33,30L 24,38L 33,46L 23,50L 27,58L 45,58L 55,38L 45,18L 27,18 Z")
}
}
};
await RenderToFile(target);
CompareImages();
}
} }
} }

BIN
tests/TestFiles/Direct2D1/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
tests/TestFiles/Skia/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

Loading…
Cancel
Save