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"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.OpacityMask> </Rectangle>
</Rectangle.OpacityMask>
</Rectangle>
<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="OrangeRed" Canvas.Left="180" Canvas.Top="250">

2
samples/ControlCatalog/SideBar.xaml

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

2
samples/RenderDemo/SideBar.xaml

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

10
samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs

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

2
samples/interop/Direct3DInteropSample/MainWindow.cs

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

44
src/Avalonia.Animation/Animatable.cs

@ -11,35 +11,10 @@ using Avalonia.Data;
namespace Avalonia.Animation
{
/// <summary>
/// Base class for control which can have property transitions.
/// Base class for all animatable objects.
/// </summary>
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>
/// Defines the <see cref="PlayState"/> property.
/// </summary>
@ -59,27 +34,25 @@ namespace Avalonia.Animation
{
get { return _playState; }
set { SetAndRaise(PlayStateProperty, ref _playState, value); }
}
/// <summary>
/// Defines the <see cref="Transitions"/> property.
/// </summary>
public static readonly DirectProperty<Animatable, IEnumerable<ITransition>> TransitionsProperty =
AvaloniaProperty.RegisterDirect<Animatable, IEnumerable<ITransition>>(
public static readonly DirectProperty<Animatable, Transitions> TransitionsProperty =
AvaloniaProperty.RegisterDirect<Animatable, Transitions>(
nameof(Transitions),
o => o.Transitions,
(o, v) => o.Transitions = v);
private IEnumerable<ITransition> _transitions = new AvaloniaList<ITransition>();
private Transitions _transitions;
/// <summary>
/// Gets or sets the property transitions for the control.
/// </summary>
public IEnumerable<ITransition> Transitions
public Transitions Transitions
{
get { return _transitions; }
get { return _transitions ?? (_transitions = new Transitions()); }
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>
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)>
{
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) )
@ -40,38 +94,6 @@ namespace Avalonia.Animation
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)
{
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;
}
internal bool isNeutral;
public Type AnimatorType { get; }
public Cue Cue { get; }
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>
/// List of type-converted keyframes.
/// </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;
/// <summary>
@ -28,18 +28,17 @@ namespace Avalonia.Animation
public Animator()
{
// Invalidate keyframes when changed.
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
}
/// <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();
return obsMatch
// Ignore triggers when global timers are paused.
.Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
return match
.Where(p => p)
.Subscribe(_ =>
{
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>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{
KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
AnimatorKeyFrame firstCue, lastCue ;
int kvCount = _convertedKeyframes.Count;
if (kvCount > 2)
{
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
if (t <= 0.0)
{
firstCue = _convertedKeyframes.First();
lastCue = _convertedKeyframes.Skip(1).First();
firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1];
}
else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0)
else if (t >= 1.0)
{
firstCue = _convertedKeyframes.Skip(kvCount - 2).First();
lastCue = _convertedKeyframes.Last();
firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1];
}
else
{
firstCue = _convertedKeyframes.Last(j => j.Key <= t);
lastCue = _convertedKeyframes.First(j => j.Key >= t);
(double time, int index) maxval = (0.0d, 0);
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
{
firstCue = _convertedKeyframes.First();
lastCue = _convertedKeyframes.Last();
firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1];
}
double t0 = firstCue.Key;
double t1 = lastCue.Key;
double t0 = firstCue.Cue.CueValue;
double t1 = lastCue.Cue.CueValue;
var intraframeTime = (t - t0) / (t1 - t0);
var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral);
var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral);
var firstFrameData = (firstCue.GetTypedValue<T>(), firstCue.isNeutral);
var lastFrameData = (lastCue.GetTypedValue<T>(), lastCue.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete)
{
var stateMachine = new AnimatorStateMachine<T>(animation, control, this, onComplete);
Timing.AnimationStateTimer
.TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => stateMachine.Step(p, DoInterpolation));
return control.Bind(Property, stateMachine, BindingPriority.Animation);
var instance = new AnimationInstance<T>(animation, control, this, onComplete, DoInterpolation);
return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
}
/// <summary>
@ -111,18 +113,26 @@ namespace Avalonia.Animation
protected abstract T DoInterpolation(double time, T neutralValue);
/// <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>
private void VerifyConvertKeyFrames()
{
foreach (AnimatorKeyFrame keyframe in this)
{
_convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
_convertedKeyframes.Add(keyframe);
}
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()
@ -130,14 +140,14 @@ namespace Avalonia.Animation
bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false;
// Make start and end keyframe mandatory.
foreach (var converted in _convertedKeyframes.Keys)
// Check if there's start and end keyframes.
foreach (var frame in _convertedKeyframes)
{
if (DoubleUtils.AboutEqual(converted, 0.0))
if (frame.Cue.CueValue == 0.0d)
{
hasStartKey = true;
}
else if (DoubleUtils.AboutEqual(converted, 1.0))
else if (frame.Cue.CueValue == 1.0d)
{
hasEndKey = true;
}
@ -151,12 +161,12 @@ namespace Avalonia.Animation
{
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)
{
_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
{
/// <summary>
/// A Cue object for <see cref="KeyFrame"/>.
/// Determines the time index for a <see cref="KeyFrame"/>.
/// </summary>
[TypeConverter(typeof(CueTypeConverter))]
public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
@ -82,5 +82,4 @@ namespace Avalonia.Animation
return Cue.Parse((string)value, culture);
}
}
}
}

79
src/Avalonia.Animation/Timing.cs

@ -13,9 +13,6 @@ namespace Avalonia.Animation
/// </summary>
public static class Timing
{
static ulong _transitionsFrameCount;
static PlayState _globalState = PlayState.Run;
/// <summary>
/// The number of frames per second.
/// </summary>
@ -30,41 +27,16 @@ namespace Avalonia.Animation
/// Initializes static members of the <see cref="Timing"/> class.
/// </summary>
static Timing()
{
{
var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance);
AnimationStateTimer = globalTimer
.Select(_ =>
{
return _globalState;
})
AnimationsTimer = globalTimer
.Select(_ => GetTickCount())
.Publish()
.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>
/// Gets the animation play state for all animations
/// </summary>
public static PlayState GetGlobalPlayState()
{
Dispatcher.UIThread.VerifyAccess();
return _globalState;
}
internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount);
/// <summary>
/// Gets the animation timer.
@ -74,48 +46,9 @@ namespace Avalonia.Animation
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the current playstate of the animation.
/// </remarks>
internal static IObservable<PlayState> AnimationStateTimer
internal static IObservable<TimeSpan> AnimationsTimer
{
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.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
namespace Avalonia.Animation
{
@ -23,17 +24,7 @@ namespace Avalonia.Animation
/// <summary>
/// Gets the easing class to be used.
/// </summary>
public Easing Easing
{
get
{
return _easing ?? (_easing = new LinearEasing());
}
set
{
_easing = value;
}
}
public Easing Easing { get; set; } = new LinearEasing();
/// <inheritdocs/>
public AvaloniaProperty Property
@ -60,8 +51,10 @@ namespace Avalonia.Animation
/// <inheritdocs/>
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);
return control.Bind(Property, transition, Data.BindingPriority.Animation);
var transition = DoTransition(new TransitionInstance(Duration), (T)oldValue, (T)newValue);
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>
/// Half of <see cref="Math.PI"/>
/// </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>
static Border()
{
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure(BorderThicknessProperty);
AffectsRender<Border>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
/// <summary>

2
src/Avalonia.Controls/Button.cs

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

6
src/Avalonia.Controls/ButtonSpinner.cs

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

26
src/Avalonia.Controls/Canvas.cs

@ -48,7 +48,7 @@ namespace Avalonia.Controls
static Canvas()
{
ClipToBoundsProperty.OverrideDefaultValue<Canvas>(false);
AffectsCanvasArrange(LeftProperty, TopProperty, RightProperty, BottomProperty);
AffectsParentArrange<Canvas>(LeftProperty, TopProperty, RightProperty, BottomProperty);
}
/// <summary>
@ -207,29 +207,5 @@ namespace Avalonia.Controls
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()
{
ContentControlMixin.Attach<ContentControl>(ContentProperty, x => x.LogicalChildren);
PseudoClass(ContentProperty, x => x != null, ":valid");
PseudoClass(ContentProperty, x => x == null, ":invalid");
PseudoClass<ContentControl, object>(ContentProperty, x => x != null, ":valid");
PseudoClass<ContentControl, object>(ContentProperty, x => x == null, ":invalid");
}
/// <summary>

2
src/Avalonia.Controls/Decorator.cs

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

4
src/Avalonia.Controls/DockPanel.cs

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

6
src/Avalonia.Controls/DrawingPresenter.cs

@ -8,8 +8,8 @@ namespace Avalonia.Controls
{
static DrawingPresenter()
{
AffectsMeasure(DrawingProperty);
AffectsRender(DrawingProperty);
AffectsMeasure<DrawingPresenter>(DrawingProperty);
AffectsRender<DrawingPresenter>(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()
{
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left");
PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right");
PseudoClass(IsExpandedProperty, ":expanded");
PseudoClass<Expander>(IsExpandedProperty, ":expanded");
IsExpandedProperty.Changed.AddClassHandler<Expander>(x => x.OnIsExpandedChanged);
}

5
src/Avalonia.Controls/Grid.cs

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

3
src/Avalonia.Controls/Image.cs

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

42
src/Avalonia.Controls/Panel.cs

@ -72,6 +72,32 @@ namespace Avalonia.Controls
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>
/// Called when the <see cref="Children"/> collection changes.
/// </summary>
@ -116,5 +142,21 @@ namespace Avalonia.Controls
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>
static ContentPresenter()
{
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure(BorderThicknessProperty, PaddingProperty);
AffectsRender<ContentPresenter>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure<ContentPresenter>(BorderThicknessProperty, PaddingProperty);
ContentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.TemplatedParentChanged);

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

@ -279,6 +279,6 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Invalidates the current scroll.
/// </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);
ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>(x => x.ChildChanged);
AffectsArrange(OffsetProperty);
AffectsArrange<ScrollContentPresenter>(OffsetProperty);
}
/// <summary>

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

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

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

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

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

@ -54,8 +54,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
static ScrollBar()
{
PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>(o => o.OnThumbDragDelta, 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()
{
PseudoClass(IsCheckedProperty, c => c == true, ":checked");
PseudoClass(IsCheckedProperty, c => c == false, ":unchecked");
PseudoClass(IsCheckedProperty, c => c == null, ":indeterminate");
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == true, ":checked");
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == false, ":unchecked");
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == null, ":indeterminate");
}
public bool? IsChecked

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

@ -39,10 +39,12 @@ namespace Avalonia.Controls.Primitives
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);
IncreaseButtonProperty.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

6
src/Avalonia.Controls/ProgressBar.cs

@ -33,9 +33,9 @@ namespace Avalonia.Controls
static ProgressBar()
{
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
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;
bool _calculateTransformOnArrange = false;
static Shape()
{
AffectsMeasure(StretchProperty, StrokeThicknessProperty);
AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty);
AffectsMeasure<Shape>(StretchProperty, StrokeThicknessProperty);
AffectsRender<Shape>(FillProperty, StrokeProperty, StrokeDashArrayProperty);
}
public Geometry DefiningGeometry

2
src/Avalonia.Controls/Slider.cs

@ -42,6 +42,8 @@ namespace Avalonia.Controls
static Slider()
{
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.DragDeltaEvent.AddClassHandler<Slider>(x => x.OnThumbDragDelta, 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>
static StackPanel()
{
AffectsMeasure(SpacingProperty);
AffectsMeasure(OrientationProperty);
AffectsMeasure<StackPanel>(SpacingProperty);
AffectsMeasure<StackPanel>(OrientationProperty);
}
/// <summary>

2
src/Avalonia.Controls/TabControl.cs

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

9
src/Avalonia.Controls/TextBlock.cs

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

2
src/Avalonia.Controls/TopLevel.cs

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

2
src/Avalonia.Controls/WrapPanel.cs

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

6
src/Avalonia.Input/InputElement.cs

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

68
src/Avalonia.Layout/Layoutable.cs

@ -140,7 +140,7 @@ namespace Avalonia.Layout
/// </summary>
static Layoutable()
{
AffectsMeasure(
AffectsMeasure<Layoutable>(
IsVisibleProperty,
WidthProperty,
HeightProperty,
@ -427,11 +427,32 @@ namespace Avalonia.Layout
/// 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>
[Obsolete("Use AffectsMeasure<T> and specify the control type.")]
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)
{
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
/// property will cause <see cref="InvalidateArrange"/> to be called on the element.
/// </remarks>
[Obsolete("Use AffectsArrange<T> and specify the control type.")]
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)
{
property.Changed.Subscribe(AffectsArrangeInvalidate);
property.Changed.Subscribe(Invalidate);
}
}
@ -632,26 +674,6 @@ namespace Avalonia.Layout
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>
/// Tests whether any of a <see cref="Rect"/>'s properties include negative values,
/// a NaN or Infinity.

45
src/Avalonia.Styling/StyledElement.cs

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

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

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

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

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

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

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

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

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

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

@ -30,19 +30,20 @@ namespace Avalonia.Rendering.SceneGraph
Matrix Transform { get; }
/// <summary>
/// Gets the bounds for the node's geometry in global coordinates.
/// Gets the bounds of the node's geometry in global coordinates.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Gets the clip bounds for the node in global coordinates.
/// </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; }
/// <summary>
/// Gets the layout bounds for the node in global coordinates.
/// </summary>
Rect LayoutBounds { get; }
/// <summary>
/// Whether the node is clipped to <see cref="ClipBounds"/>.
/// </summary>

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

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

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

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

36
src/Avalonia.Visuals/Visual.cs

@ -100,7 +100,7 @@ namespace Avalonia
/// </summary>
static Visual()
{
AffectsRender(
AffectsRender<Visual>(
BoundsProperty,
ClipProperty,
ClipToBoundsProperty,
@ -320,11 +320,34 @@ namespace Avalonia
/// on the control which when changed should cause a redraw. This is similar to WPF's
/// FrameworkPropertyMetadata.AffectsRender flag.
/// </remarks>
[Obsolete("Use AffectsRender<T> and specify the control type.")]
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)
{
property.Changed.Subscribe(AffectsRenderInvalidate);
property.Changed.Subscribe(Invalidate);
}
}
@ -412,15 +435,6 @@ namespace Avalonia
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>
/// Gets the visual offset from the specified ancestor.
/// </summary>

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

@ -1,7 +1,6 @@
// 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 SharpDX;
using SharpDX.Direct2D1;
using SharpDX.DirectWrite;
@ -9,7 +8,7 @@ using SharpDX.Mathematics.Interop;
namespace Avalonia.Direct2D1.Media
{
internal class AvaloniaTextRenderer : TextRenderer
internal class AvaloniaTextRenderer : TextRendererBase
{
private readonly DrawingContextImpl _context;
@ -27,18 +26,7 @@ namespace Avalonia.Direct2D1.Media
_foreground = foreground;
}
public IDisposable Shadow
{
get;
set;
}
public void Dispose()
{
Shadow?.Dispose();
}
public Result DrawGlyphRun(
public override Result DrawGlyphRun(
object clientDrawingContext,
float baselineOriginX,
float baselineOriginY,
@ -68,34 +56,14 @@ namespace Avalonia.Direct2D1.Media
return Result.Ok;
}
public Result DrawInlineObject(object clientDrawingContext, float originX, float originY, InlineObject inlineObject, bool isSideways, bool isRightToLeft, ComObject clientDrawingEffect)
{
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)
public override RawMatrix3x2 GetCurrentTransform(object clientDrawingContext)
{
return _renderTarget.Transform;
}
public float GetPixelsPerDip(object clientDrawingContext)
public override float GetPixelsPerDip(object clientDrawingContext)
{
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;
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)
using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface))
{
TextAlignment = textAlignment.ToDirect2D()
};
textFormat.Dispose();
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()
};
}
if (spans != null)
{
@ -108,6 +107,7 @@ namespace Avalonia.Direct2D1.Media
private Size Measure()
{
var metrics = TextLayout.Metrics;
var width = metrics.WidthIncludingTrailingWhitespace;
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 SharpDX.Direct2D1;
using SharpDX.WIC;
using Bitmap = SharpDX.Direct2D1.Bitmap;
namespace Avalonia.Direct2D1.Media
{
using SharpDX.WIC;
using Bitmap = SharpDX.Direct2D1.Bitmap;
using PixelFormat = SharpDX.Direct2D1.PixelFormat;
/// <summary>
/// A Direct2D Bitmap implementation that uses a GPU memory bitmap as its image.
/// </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, 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.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);
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