Browse Source

Merge branch 'master' into demo/dynamic-menus

pull/1893/head
danwalmsley 7 years ago
committed by GitHub
parent
commit
a966efd0e8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 16
      samples/RenderDemo/Pages/AnimationsPage.xaml.cs
  3. 26
      samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
  4. 3
      src/Android/Avalonia.Android/AndroidPlatform.cs
  5. 28
      src/Avalonia.Animation/Animatable.cs
  6. 15
      src/Avalonia.Animation/Animation.cs
  7. 61
      src/Avalonia.Animation/AnimationInstance`1.cs
  8. 118
      src/Avalonia.Animation/Animator`1.cs
  9. 30
      src/Avalonia.Animation/Clock.cs
  10. 72
      src/Avalonia.Animation/ClockBase.cs
  11. 65
      src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs
  12. 5
      src/Avalonia.Animation/DoubleAnimator.cs
  13. 5
      src/Avalonia.Animation/FillMode.cs
  14. 11
      src/Avalonia.Animation/IAnimation.cs
  15. 3
      src/Avalonia.Animation/IAnimationSetter.cs
  16. 7
      src/Avalonia.Animation/IAnimator.cs
  17. 11
      src/Avalonia.Animation/IClock.cs
  18. 10
      src/Avalonia.Animation/IGlobalClock.cs
  19. 2
      src/Avalonia.Animation/ITransition.cs
  20. 5
      src/Avalonia.Animation/KeyFrame.cs
  21. 3
      src/Avalonia.Animation/KeyFramePair`1.cs
  22. 5
      src/Avalonia.Animation/PlayState.cs
  23. 5
      src/Avalonia.Animation/PlaybackDirection.cs
  24. 54
      src/Avalonia.Animation/Timing.cs
  25. 27
      src/Avalonia.Animation/TransitionInstance.cs
  26. 7
      src/Avalonia.Animation/Transition`1.cs
  27. 7
      src/Avalonia.Base/PriorityBindingEntry.cs
  28. 14
      src/Avalonia.Base/PriorityLevel.cs
  29. 10
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  30. 5
      src/Avalonia.Base/Reactive/ObservableEx.cs
  31. 2
      src/Avalonia.Controls/AppBuilderBase.cs
  32. 7
      src/Avalonia.Controls/Application.cs
  33. 6
      src/Avalonia.Controls/Border.cs
  34. 20
      src/Avalonia.Controls/MenuItem.cs
  35. 1
      src/Avalonia.Controls/Panel.cs
  36. 11
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  37. 8
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  38. 7
      src/Avalonia.Controls/ProgressBar.cs
  39. 4
      src/Avalonia.Controls/TextBlock.cs
  40. 1
      src/Avalonia.Controls/TopLevel.cs
  41. 3
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  42. 8
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  43. 1
      src/Avalonia.Diagnostics/DevTools.xaml
  44. 23
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  45. 38
      src/Avalonia.Diagnostics/Models/EventChainLink.cs
  46. 5
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  47. 61
      src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
  48. 98
      src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
  49. 78
      src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
  50. 60
      src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
  51. 80
      src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
  52. 53
      src/Avalonia.Diagnostics/Views/EventsView.xaml
  53. 32
      src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
  54. 19
      src/Avalonia.Styling/Styling/Style.cs
  55. 26
      src/Avalonia.Visuals/Animation/RenderLoopClock.cs
  56. 20
      src/Avalonia.Visuals/Animation/TransformAnimator.cs
  57. 1
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  58. 36
      src/Avalonia.Visuals/Media/Brush.cs
  59. 65
      src/Avalonia.Visuals/Media/GradientBrush.cs
  60. 40
      src/Avalonia.Visuals/Media/GradientStop.cs
  61. 23
      src/Avalonia.Visuals/Media/GradientStops.cs
  62. 16
      src/Avalonia.Visuals/Media/IAffectsRender.cs
  63. 4
      src/Avalonia.Visuals/Media/IGradientBrush.cs
  64. 18
      src/Avalonia.Visuals/Media/IGradientStop.cs
  65. 6
      src/Avalonia.Visuals/Media/IMutableBrush.cs
  66. 12
      src/Avalonia.Visuals/Media/ImageBrush.cs
  67. 9
      src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs
  68. 20
      src/Avalonia.Visuals/Media/Immutable/ImmutableGradientStop.cs
  69. 4
      src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs
  70. 4
      src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs
  71. 13
      src/Avalonia.Visuals/Media/LinearGradientBrush.cs
  72. 10
      src/Avalonia.Visuals/Media/RadialGradientBrush.cs
  73. 13
      src/Avalonia.Visuals/Media/SolidColorBrush.cs
  74. 7
      src/Avalonia.Visuals/Media/TileBrush.cs
  75. 12
      src/Avalonia.Visuals/Media/VisualBrush.cs
  76. 24
      src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs
  77. 93
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  78. 12
      src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs
  79. 27
      src/Avalonia.Visuals/Rendering/IRenderLoop.cs
  80. 12
      src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs
  81. 20
      src/Avalonia.Visuals/Rendering/IRenderTimer.cs
  82. 11
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  83. 120
      src/Avalonia.Visuals/Rendering/RenderLoop.cs
  84. 19
      src/Avalonia.Visuals/Visual.cs
  85. 3
      src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs
  86. 3
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  87. 7
      src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
  88. 31
      src/OSX/Avalonia.MonoMac/RenderLoop.cs
  89. 28
      src/OSX/Avalonia.MonoMac/RenderTimer.cs
  90. 25
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  91. 35
      src/Windows/Avalonia.Win32/RenderLoop.cs
  92. 56
      src/Windows/Avalonia.Win32/RenderTimer.cs
  93. 4
      src/Windows/Avalonia.Win32/Win32Platform.cs
  94. 9
      src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs
  95. 3
      src/iOS/Avalonia.iOS/iOSPlatform.cs
  96. 14
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  97. 21
      tests/Avalonia.Controls.UnitTests/BorderTests.cs
  98. 26
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  99. 21
      tests/Avalonia.Controls.UnitTests/PanelTests.cs
  100. 20
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs

7
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -107,9 +107,12 @@
</UserControl.Styles> </UserControl.Styles>
<Grid> <Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False">
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock> <TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding ToggleGlobalPlayState}"/> <Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel> </StackPanel>
<WrapPanel ClipToBounds="False"> <WrapPanel ClipToBounds="False">
<Border Classes="Test Rect1" Background="DarkRed"/> <Border Classes="Test Rect1" Background="DarkRed"/>
@ -120,4 +123,4 @@
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

16
samples/RenderDemo/Pages/AnimationsPage.xaml.cs

@ -5,6 +5,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using RenderDemo.ViewModels; using RenderDemo.ViewModels;
@ -23,5 +24,20 @@ namespace RenderDemo.Pages
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private void ToggleClock(object sender, RoutedEventArgs args)
{
var button = sender as Button;
var clock = button.Clock;
if (clock.PlayState == PlayState.Run)
{
clock.PlayState = PlayState.Pause;
}
else if (clock.PlayState == PlayState.Pause)
{
clock.PlayState = PlayState.Run;
}
}
} }
} }

26
samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs

@ -6,27 +6,15 @@ namespace RenderDemo.ViewModels
{ {
public class AnimationsPageViewModel : ReactiveObject public class AnimationsPageViewModel : ReactiveObject
{ {
private string _playStateText = "Pause all animations"; private bool _isPlaying = true;
public AnimationsPageViewModel() private string _playStateText = "Pause animations on this page";
{
ToggleGlobalPlayState = ReactiveCommand.Create(() => TogglePlayState());
}
void TogglePlayState() public void TogglePlayState()
{ {
switch (Animation.GlobalPlayState) PlayStateText = _isPlaying
{ ? "Resume animations on this page" : "Pause animations on this page";
case PlayState.Run: _isPlaying = !_isPlaying;
PlayStateText = "Resume all animations";
Animation.GlobalPlayState = PlayState.Pause;
break;
case PlayState.Pause:
PlayStateText = "Pause all animations";
Animation.GlobalPlayState = PlayState.Run;
break;
}
} }
public string PlayStateText public string PlayStateText
@ -34,7 +22,5 @@ namespace RenderDemo.ViewModels
get { return _playStateText; } get { return _playStateText; }
set { this.RaiseAndSetIfChanged(ref _playStateText, value); } set { this.RaiseAndSetIfChanged(ref _playStateText, value); }
} }
public ReactiveCommand ToggleGlobalPlayState { get; }
} }
} }

3
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -52,7 +52,8 @@ namespace Avalonia.Android
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>() .Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(Instance) .Bind<IWindowingPlatform>().ToConstant(Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60)) .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IAssetLoader>().ToConstant(new AssetLoader(app.GetType().Assembly)); .Bind<IAssetLoader>().ToConstant(new AssetLoader(app.GetType().Assembly));
SkiaPlatform.Initialize(); SkiaPlatform.Initialize();

28
src/Avalonia.Animation/Animatable.cs

@ -14,26 +14,14 @@ namespace Avalonia.Animation
/// Base class for all animatable objects. /// Base class for all animatable objects.
/// </summary> /// </summary>
public class Animatable : AvaloniaObject public class Animatable : AvaloniaObject
{ {
/// <summary> public static readonly StyledProperty<IClock> ClockProperty =
/// Defines the <see cref="PlayState"/> property. AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
/// </summary>
public static readonly DirectProperty<Animatable, PlayState> PlayStateProperty =
AvaloniaProperty.RegisterDirect<Animatable, PlayState>(
nameof(PlayState),
o => o.PlayState,
(o, v) => o.PlayState = v);
private PlayState _playState = PlayState.Run;
/// <summary> public IClock Clock
/// Gets or sets the state of the animation for this
/// control.
/// </summary>
public PlayState PlayState
{ {
get { return _playState; } get => GetValue(ClockProperty);
set { SetAndRaise(PlayStateProperty, ref _playState, value); } set => SetValue(ClockProperty, value);
} }
/// <summary> /// <summary>
@ -69,9 +57,9 @@ namespace Avalonia.Animation
if (match != null) if (match != null)
{ {
match.Apply(this, e.OldValue, e.NewValue); match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
} }
} }
} }
} }
} }

15
src/Avalonia.Animation/Animation.cs

@ -17,11 +17,6 @@ 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> /// <summary>
/// Gets or sets the active time of this animation. /// Gets or sets the active time of this animation.
/// </summary> /// </summary>
@ -149,12 +144,12 @@ namespace Avalonia.Animation
} }
/// <inheritdocs/> /// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete) public IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
{ {
var (animators, subscriptions) = InterpretKeyframes(control); var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1) if (animators.Count == 1)
{ {
subscriptions.Add(animators[0].Apply(this, control, match, onComplete)); subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete));
} }
else else
{ {
@ -168,7 +163,7 @@ namespace Avalonia.Animation
animatorOnComplete = () => tcs.SetResult(null); animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task); completionTasks.Add(tcs.Task);
} }
subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete)); subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete));
} }
if (onComplete != null) if (onComplete != null)
@ -180,7 +175,7 @@ namespace Avalonia.Animation
} }
/// <inheritdocs/> /// <inheritdocs/>
public Task RunAsync(Animatable control) public Task RunAsync(Animatable control, IClock clock = null)
{ {
var run = new TaskCompletionSource<object>(); var run = new TaskCompletionSource<object>();
@ -188,7 +183,7 @@ namespace Avalonia.Animation
run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null; IDisposable subscriptions = null;
subscriptions = this.Apply(control, Observable.Return(true), () => subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{ {
run.SetResult(null); run.SetResult(null);
subscriptions?.Dispose(); subscriptions?.Dispose();

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

@ -8,7 +8,7 @@ using Avalonia.Reactive;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Handles interpolatoin and time-related functions /// Handles interpolation and time-related functions
/// for keyframe animations. /// for keyframe animations.
/// </summary> /// </summary>
internal class AnimationInstance<T> : SingleSubscriberObservableBase<T> internal class AnimationInstance<T> : SingleSubscriberObservableBase<T>
@ -19,7 +19,6 @@ namespace Avalonia.Animation
private double _currentIteration; private double _currentIteration;
private bool _isLooping; private bool _isLooping;
private bool _gotFirstKFValue; private bool _gotFirstKFValue;
private bool _gotFirstFrameCount;
private bool _iterationDelay; private bool _iterationDelay;
private FillMode _fillMode; private FillMode _fillMode;
private PlaybackDirection _animationDirection; private PlaybackDirection _animationDirection;
@ -29,15 +28,14 @@ namespace Avalonia.Animation
private double _speedRatio; private double _speedRatio;
private TimeSpan _delay; private TimeSpan _delay;
private TimeSpan _duration; private TimeSpan _duration;
private TimeSpan _firstFrameCount;
private TimeSpan _internalClock;
private TimeSpan? _previousClock;
private Easings.Easing _easeFunc; private Easings.Easing _easeFunc;
private Action _onCompleteAction; private Action _onCompleteAction;
private Func<double, T, T> _interpolator; private Func<double, T, T> _interpolator;
private IDisposable _timerSubscription; private IDisposable _timerSubscription;
private readonly IClock _baseClock;
private IClock _clock;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, Action OnComplete, Func<double, T, T> Interpolator) public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
{ {
if (animation.SpeedRatio <= 0) if (animation.SpeedRatio <= 0)
throw new InvalidOperationException("Speed ratio cannot be negative or zero."); throw new InvalidOperationException("Speed ratio cannot be negative or zero.");
@ -73,17 +71,19 @@ namespace Avalonia.Animation
_fillMode = animation.FillMode; _fillMode = animation.FillMode;
_onCompleteAction = OnComplete; _onCompleteAction = OnComplete;
_interpolator = Interpolator; _interpolator = Interpolator;
_baseClock = baseClock;
} }
protected override void Unsubscribed() protected override void Unsubscribed()
{ {
_timerSubscription?.Dispose(); _timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
} }
protected override void Subscribed() protected override void Subscribed()
{ {
_timerSubscription = Timing.AnimationsTimer _clock = new Clock(_baseClock);
.Subscribe(p => this.Step(p)); _timerSubscription = _clock.Subscribe(Step);
} }
public void Step(TimeSpan frameTick) public void Step(TimeSpan frameTick)
@ -116,46 +116,21 @@ namespace Avalonia.Animation
PublishNext(_lastInterpValue); PublishNext(_lastInterpValue);
} }
private void DoPlayStatesAndTime(TimeSpan systemTime) private void DoPlayStates()
{ {
if (Animation.GlobalPlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop) if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
DoComplete(); 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) if (!_gotFirstKFValue)
{ {
_firstKFValue = (T)_parent.First().Value; _firstKFValue = (T)_parent.First().Value;
_gotFirstKFValue = true; _gotFirstKFValue = true;
} }
if (!_gotFirstFrameCount)
{
_firstFrameCount = _internalClock;
_gotFirstFrameCount = true;
}
} }
private void InternalStep(TimeSpan systemTime) private void InternalStep(TimeSpan time)
{ {
DoPlayStatesAndTime(systemTime); DoPlayStates();
var time = _internalClock - _firstFrameCount;
var delayEndpoint = _delay; var delayEndpoint = _delay;
var iterationEndpoint = delayEndpoint + _duration; var iterationEndpoint = delayEndpoint + _duration;
@ -176,22 +151,18 @@ namespace Avalonia.Animation
} }
//Calculate the current iteration number //Calculate the current iteration number
_currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2; _currentIteration = (int)Math.Floor((double)((double)time.Ticks / iterationEndpoint.Ticks)) + 2;
} }
else else
{ {
_previousClock = systemTime;
return; return;
} }
time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks); time = TimeSpan.FromTicks((long)(time.Ticks % iterationEndpoint.Ticks));
if (!_isLooping) if (!_isLooping)
{ {
if (_currentIteration > _repeatCount) if ((_currentIteration > _repeatCount) || (time > iterationEndpoint))
DoComplete();
if (time > iterationEndpoint)
DoComplete(); DoComplete();
} }
@ -225,4 +196,4 @@ namespace Avalonia.Animation
} }
} }
} }
} }

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

@ -1,10 +1,14 @@
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.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Animation.Utils; using Avalonia.Animation.Utils;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -17,7 +21,7 @@ namespace Avalonia.Animation
/// List of type-converted keyframes. /// List of type-converted keyframes.
/// </summary> /// </summary>
private readonly List<AnimatorKeyFrame> _convertedKeyframes = new List<AnimatorKeyFrame>(); private readonly List<AnimatorKeyFrame> _convertedKeyframes = new List<AnimatorKeyFrame>();
private bool _isVerifiedAndConverted; private bool _isVerifiedAndConverted;
/// <summary> /// <summary>
@ -28,21 +32,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> match, Action onComplete) public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
{ {
if (!_isVerifiedAndConverted) if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames(); VerifyConvertKeyFrames();
return match var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete);
.Where(p => p) return match.Subscribe(subject);
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control, onComplete);
});
} }
/// <summary> /// <summary>
@ -52,58 +52,84 @@ namespace Avalonia.Animation
/// (i.e., the normalized time between the selected keyframes, relative to the /// (i.e., the normalized time between the selected keyframes, relative to the
/// time parameter). /// time parameter).
/// </summary> /// </summary>
/// <param name="t">The time parameter, relative to the total animation time</param> /// <param name="animationTime">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 animationTime)
{ {
AnimatorKeyFrame firstCue, lastCue ; AnimatorKeyFrame firstKeyframe, lastKeyframe;
int kvCount = _convertedKeyframes.Count; int kvCount = _convertedKeyframes.Count;
if (kvCount > 2) if (kvCount > 2)
{ {
if (t <= 0.0) if (animationTime <= 0.0)
{ {
firstCue = _convertedKeyframes[0]; firstKeyframe = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1]; lastKeyframe = _convertedKeyframes[1];
} }
else if (t >= 1.0) else if (animationTime >= 1.0)
{ {
firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2]; firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1]; lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1];
} }
else else
{ {
(double time, int index) maxval = (0.0d, 0); int index = FindClosestBeforeKeyFrame(animationTime);
for (int i = 0; i < _convertedKeyframes.Count; i++) firstKeyframe = _convertedKeyframes[index];
{ lastKeyframe = _convertedKeyframes[index + 1];
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[0]; firstKeyframe = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1]; lastKeyframe = _convertedKeyframes[1];
} }
double t0 = firstCue.Cue.CueValue; double t0 = firstKeyframe.Cue.CueValue;
double t1 = lastCue.Cue.CueValue; double t1 = lastKeyframe.Cue.CueValue;
var intraframeTime = (t - t0) / (t1 - t0); var intraframeTime = (animationTime - t0) / (t1 - t0);
var firstFrameData = (firstCue.GetTypedValue<T>(), firstCue.isNeutral); var firstFrameData = (firstKeyframe.GetTypedValue<T>(), firstKeyframe.isNeutral);
var lastFrameData = (lastCue.GetTypedValue<T>(), lastCue.isNeutral); var lastFrameData = (lastKeyframe.GetTypedValue<T>(), lastKeyframe.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData)); return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
} }
private int FindClosestBeforeKeyFrame(double time)
{
int FindClosestBeforeKeyFrame(int startIndex, int length)
{
if (length == 0 || length == 1)
{
return startIndex;
}
int middle = startIndex + (length / 2);
if (_convertedKeyframes[middle].Cue.CueValue < time)
{
return FindClosestBeforeKeyFrame(middle, length - middle);
}
else if (_convertedKeyframes[middle].Cue.CueValue > time)
{
return FindClosestBeforeKeyFrame(startIndex, middle - startIndex);
}
else
{
return middle;
}
}
return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count);
}
/// <summary> /// <summary>
/// Runs the KeyFrames Animation. /// Runs the KeyFrames Animation.
/// </summary> /// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete)
{ {
var instance = new AnimationInstance<T>(animation, control, this, onComplete, DoInterpolation); var instance = new AnimationInstance<T>(
animation,
control,
this,
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
DoInterpolation);
return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation); return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
} }
@ -124,14 +150,6 @@ namespace Avalonia.Animation
AddNeutralKeyFramesIfNeeded(); AddNeutralKeyFramesIfNeeded();
var copy = _convertedKeyframes.ToList().OrderBy(p => p.Cue.CueValue);
_convertedKeyframes.Clear();
foreach (AnimatorKeyFrame keyframe in copy)
{
_convertedKeyframes.Add(keyframe);
}
_isVerifiedAndConverted = true; _isVerifiedAndConverted = true;
} }
@ -161,7 +179,7 @@ namespace Avalonia.Animation
{ {
if (!hasStartKey) if (!hasStartKey)
{ {
_convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true }); _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
} }
if (!hasEndKey) if (!hasEndKey)
@ -170,4 +188,4 @@ namespace Avalonia.Animation
} }
} }
} }
} }

30
src/Avalonia.Animation/Clock.cs

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
public class Clock : ClockBase
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
private IDisposable _parentSubscription;
public Clock()
:this(GlobalClock)
{
}
public Clock(IClock parent)
{
_parentSubscription = parent.Subscribe(Pulse);
}
protected override void Stop()
{
_parentSubscription?.Dispose();
}
}
}

72
src/Avalonia.Animation/ClockBase.cs

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
public class ClockBase : IClock
{
private ClockObservable _observable;
private IObservable<TimeSpan> _connectedObservable;
private TimeSpan? _previousTime;
private TimeSpan _internalTime;
protected ClockBase()
{
_observable = new ClockObservable();
_connectedObservable = _observable.Publish().RefCount();
}
protected bool HasSubscriptions => _observable.HasSubscriptions;
public PlayState PlayState { get; set; }
protected void Pulse(TimeSpan systemTime)
{
if (!_previousTime.HasValue)
{
_previousTime = systemTime;
_internalTime = TimeSpan.Zero;
}
else
{
if (PlayState == PlayState.Pause)
{
_previousTime = systemTime;
return;
}
var delta = systemTime - _previousTime;
_internalTime += delta.Value;
_previousTime = systemTime;
}
_observable.Pulse(_internalTime);
if (PlayState == PlayState.Stop)
{
Stop();
}
}
protected virtual void Stop()
{
}
public IDisposable Subscribe(IObserver<TimeSpan> observer)
{
return _connectedObservable.Subscribe(observer);
}
private class ClockObservable : LightweightObservableBase<TimeSpan>
{
public bool HasSubscriptions { get; private set; }
public void Pulse(TimeSpan time) => PublishNext(time);
protected override void Initialize() => HasSubscriptions = true;
protected override void Deinitialize() => HasSubscriptions = false;
}
}
}

65
src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs

@ -0,0 +1,65 @@
// 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.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
/// <summary>
/// Manages the lifetime of animation instances as determined by its selector state.
/// </summary>
internal class DisposeAnimationInstanceSubject<T> : IObserver<bool>, IDisposable
{
private IDisposable _lastInstance;
private bool _lastMatch;
private Animator<T> _animator;
private Animation _animation;
private Animatable _control;
private Action _onComplete;
private IClock _clock;
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock clock, Action onComplete)
{
this._animator = animator;
this._animation = animation;
this._control = control;
this._onComplete = onComplete;
this._clock = clock;
}
public void Dispose()
{
_lastInstance?.Dispose();
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
_lastInstance?.Dispose();
}
void IObserver<bool>.OnNext(bool matchVal)
{
if (matchVal != _lastMatch)
{
_lastInstance?.Dispose();
if (matchVal)
{
_lastInstance = _animator.Run(_animation, _control, _clock, _onComplete);
}
_lastMatch = matchVal;
}
}
}
}

5
src/Avalonia.Animation/DoubleAnimator.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation // 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.
namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Animator that handles <see cref="double"/> properties. /// Animator that handles <see cref="double"/> properties.

5
src/Avalonia.Animation/FillMode.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation // 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.
namespace Avalonia.Animation
{ {
public enum FillMode public enum FillMode
{ {

11
src/Avalonia.Animation/IAnimation.cs

@ -1,3 +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 System;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,13 +12,13 @@ namespace Avalonia.Animation
public interface IAnimation public interface IAnimation
{ {
/// <summary> /// <summary>
/// Apply the animation to the specified control /// Apply the animation to the specified control and run it when <paramref name="match" /> produces <c>true</c>.
/// </summary> /// </summary>
IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete = null); IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete = null);
/// <summary> /// <summary>
/// Run the animation to the specified control /// Run the animation on the specified control.
/// </summary> /// </summary>
Task RunAsync(Animatable control); Task RunAsync(Animatable control, IClock clock);
} }
} }

3
src/Avalonia.Animation/IAnimationSetter.cs

@ -1,3 +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.
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
public interface IAnimationSetter public interface IAnimationSetter

7
src/Avalonia.Animation/IAnimator.cs

@ -1,4 +1,7 @@
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.Collections.Generic; using System.Collections.Generic;
namespace Avalonia.Animation namespace Avalonia.Animation
@ -16,6 +19,6 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Applies the current KeyFrame group to the specified control. /// Applies the current KeyFrame group to the specified control.
/// </summary> /// </summary>
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete); IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete);
} }
} }

11
src/Avalonia.Animation/IClock.cs

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
public interface IClock : IObservable<TimeSpan>
{
PlayState PlayState { get; set; }
}
}

10
src/Avalonia.Animation/IGlobalClock.cs

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
public interface IGlobalClock : IClock
{
}
}

2
src/Avalonia.Animation/ITransition.cs

@ -13,7 +13,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Applies the transition to the specified <see cref="Animatable"/>. /// Applies the transition to the specified <see cref="Animatable"/>.
/// </summary> /// </summary>
IDisposable Apply(Animatable control, object oldValue, object newValue); IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue);
/// <summary> /// <summary>
/// Gets the property to be animated. /// Gets the property to be animated.

5
src/Avalonia.Animation/KeyFrame.cs

@ -1,4 +1,7 @@
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.Collections.Generic; using System.Collections.Generic;
using Avalonia.Collections; using Avalonia.Collections;

3
src/Avalonia.Animation/KeyFramePair`1.cs

@ -1,3 +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.
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>

5
src/Avalonia.Animation/PlayState.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation // 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.
namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Determines the playback state of an animation. /// Determines the playback state of an animation.

5
src/Avalonia.Animation/PlaybackDirection.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation // 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.
namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Determines the playback direction of an animation. /// Determines the playback direction of an animation.

54
src/Avalonia.Animation/Timing.cs

@ -1,54 +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;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Threading;
namespace Avalonia.Animation
{
/// <summary>
/// Provides global timing functions for animations.
/// </summary>
public static class Timing
{
/// <summary>
/// The number of frames per second.
/// </summary>
public const int FramesPerSecond = 60;
/// <summary>
/// The time span of each frame.
/// </summary>
internal static readonly TimeSpan FrameTick = TimeSpan.FromSeconds(1.0 / FramesPerSecond);
/// <summary>
/// Initializes static members of the <see cref="Timing"/> class.
/// </summary>
static Timing()
{
var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance);
AnimationsTimer = globalTimer
.Select(_ => GetTickCount())
.Publish()
.RefCount();
}
internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount);
/// <summary>
/// Gets the animation timer.
/// </summary>
/// <remarks>
/// The animation timer triggers usually at 60 times per second or as
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the current playstate of the animation.
/// </remarks>
internal static IObservable<TimeSpan> AnimationsTimer
{
get;
}
}
}

27
src/Avalonia.Animation/TransitionInstance.cs

@ -15,21 +15,22 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double> internal class TransitionInstance : SingleSubscriberObservableBase<double>
{ {
private IDisposable timerSubscription; private IDisposable _timerSubscription;
private TimeSpan startTime; private TimeSpan _duration;
private TimeSpan duration; private readonly IClock _baseClock;
private IClock _clock;
public TransitionInstance(TimeSpan Duration) public TransitionInstance(IClock clock, TimeSpan Duration)
{ {
duration = Duration; _duration = Duration;
_baseClock = clock;
} }
private void TimerTick(TimeSpan t) private void TimerTick(TimeSpan t)
{ {
var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks; var interpVal = (double)t.Ticks / _duration.Ticks;
if (interpVal > 1d if (interpVal > 1d || interpVal < 0d)
|| interpVal < 0d)
{ {
PublishCompleted(); PublishCompleted();
return; return;
@ -40,15 +41,15 @@ namespace Avalonia.Animation
protected override void Unsubscribed() protected override void Unsubscribed()
{ {
timerSubscription?.Dispose(); _timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
} }
protected override void Subscribed() protected override void Subscribed()
{ {
startTime = Timing.GetTickCount(); _clock = new Clock(_baseClock);
timerSubscription = Timing.AnimationsTimer _timerSubscription = _clock.Subscribe(TimerTick);
.Subscribe(t => TimerTick(t));
PublishNext(0.0d); PublishNext(0.0d);
} }
} }
} }

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

@ -14,7 +14,6 @@ namespace Avalonia.Animation
public abstract class Transition<T> : AvaloniaObject, ITransition public abstract class Transition<T> : AvaloniaObject, ITransition
{ {
private AvaloniaProperty _prop; private AvaloniaProperty _prop;
private Easing _easing;
/// <summary> /// <summary>
/// Gets the duration of the animation. /// Gets the duration of the animation.
@ -49,12 +48,10 @@ namespace Avalonia.Animation
public abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue); public abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue);
/// <inheritdocs/> /// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, object oldValue, object newValue) public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue)
{ {
var transition = DoTransition(new TransitionInstance(Duration), (T)oldValue, (T)newValue); var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue);
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation); return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
} }
} }
} }

7
src/Avalonia.Base/PriorityBindingEntry.cs

@ -50,6 +50,11 @@ namespace Avalonia
get; get;
} }
/// <summary>
/// Gets a value indicating whether the binding has completed.
/// </summary>
public bool HasCompleted { get; private set; }
/// <summary> /// <summary>
/// The current value of the binding. /// The current value of the binding.
/// </summary> /// </summary>
@ -129,6 +134,8 @@ namespace Avalonia
private void Completed() private void Completed()
{ {
HasCompleted = true;
if (Dispatcher.UIThread.CheckAccess()) if (Dispatcher.UIThread.CheckAccess())
{ {
_owner.Completed(this); _owner.Completed(this);

14
src/Avalonia.Base/PriorityLevel.cs

@ -112,12 +112,16 @@ namespace Avalonia
return Disposable.Create(() => return Disposable.Create(() =>
{ {
Bindings.Remove(node); if (!entry.HasCompleted)
entry.Dispose();
if (entry.Index >= ActiveBindingIndex)
{ {
ActivateFirstBinding(); Bindings.Remove(node);
entry.Dispose();
if (entry.Index >= ActiveBindingIndex)
{
ActivateFirstBinding();
}
} }
}); });
} }

10
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -82,18 +82,10 @@ namespace Avalonia.Reactive
if (observers.Count == 0) if (observers.Count == 0)
{ {
observers.TrimExcess(); observers.TrimExcess();
Deinitialize();
} }
else
{
return;
}
} else
{
return;
} }
} }
Deinitialize();
} }
} }

5
src/Avalonia.Base/Reactive/ObservableEx.cs

@ -21,7 +21,7 @@ namespace Avalonia.Reactive
{ {
return new SingleValueImpl<T>(value); return new SingleValueImpl<T>(value);
} }
private class SingleValueImpl<T> : IObservable<T> private class SingleValueImpl<T> : IObservable<T>
{ {
private T _value; private T _value;
@ -30,7 +30,6 @@ namespace Avalonia.Reactive
{ {
_value = value; _value = value;
} }
public IDisposable Subscribe(IObserver<T> observer) public IDisposable Subscribe(IObserver<T> observer)
{ {
observer.OnNext(_value); observer.OnNext(_value);
@ -38,4 +37,4 @@ namespace Avalonia.Reactive
} }
} }
} }
} }

2
src/Avalonia.Controls/AppBuilderBase.cs

@ -272,10 +272,10 @@ namespace Avalonia.Controls
s_setupWasAlreadyCalled = true; s_setupWasAlreadyCalled = true;
Instance.RegisterServices();
RuntimePlatformServicesInitializer(); RuntimePlatformServicesInitializer();
WindowingSubsystemInitializer(); WindowingSubsystemInitializer();
RenderingSubsystemInitializer(); RenderingSubsystemInitializer();
Instance.RegisterServices();
Instance.Initialize(); Instance.Initialize();
AfterSetupCallback(Self); AfterSetupCallback(Self);
} }

7
src/Avalonia.Controls/Application.cs

@ -4,12 +4,14 @@
using System; using System;
using System.Reactive.Concurrency; using System.Reactive.Concurrency;
using System.Threading; using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
@ -335,6 +337,11 @@ namespace Avalonia
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance) .Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance)
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance) .Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance)
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>(); .Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
var clock = new RenderLoopClock();
AvaloniaLocator.CurrentMutable
.Bind<IGlobalClock>().ToConstant(clock)
.GetService<IRenderLoop>()?.Add(clock);
} }
} }
} }

6
src/Avalonia.Controls/Border.cs

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

20
src/Avalonia.Controls/MenuItem.cs

@ -99,13 +99,13 @@ namespace Avalonia.Controls
SelectableMixin.Attach<MenuItem>(IsSelectedProperty); SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
CommandProperty.Changed.Subscribe(CommandChanged); CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue<MenuItem>(true); FocusableProperty.OverrideDefaultValue<MenuItem>(true);
HeaderProperty.Changed.AddClassHandler<MenuItem>(x => x.HeaderChanged);
IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged); IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged);
IsSelectedProperty.Changed.AddClassHandler<MenuItem>(x => x.IsSelectedChanged); IsSelectedProperty.Changed.AddClassHandler<MenuItem>(x => x.IsSelectedChanged);
ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel);
ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick); ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick);
SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened); SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened);
IsSubMenuOpenProperty.Changed.AddClassHandler<MenuItem>(x => x.SubMenuOpenChanged); IsSubMenuOpenProperty.Changed.AddClassHandler<MenuItem>(x => x.SubMenuOpenChanged);
PseudoClass<MenuItem, object>(HeaderProperty, x => x as string == "-", ":separator");
} }
public MenuItem() public MenuItem()
@ -420,6 +420,24 @@ namespace Avalonia.Controls
IsEnabled = Command == null || Command.CanExecute(CommandParameter); IsEnabled = Command == null || Command.CanExecute(CommandParameter);
} }
/// <summary>
/// Called when the <see cref="Header"/> property changes.
/// </summary>
/// <param name="e">The property change event.</param>
private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.NewValue is string newValue && newValue == "-")
{
PseudoClasses.Add(":separator");
Focusable = false;
}
else if (e.OldValue is string oldValue && oldValue == "-")
{
PseudoClasses.Remove(":separator");
Focusable = true;
}
}
/// <summary> /// <summary>
/// Called when the <see cref="Icon"/> property changes. /// Called when the <see cref="Icon"/> property changes.
/// </summary> /// </summary>

1
src/Avalonia.Controls/Panel.cs

@ -30,6 +30,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static Panel() static Panel()
{ {
AffectsRender<Panel>(BackgroundProperty);
ClipToBoundsProperty.OverrideDefaultValue<Panel>(true); ClipToBoundsProperty.OverrideDefaultValue<Panel>(true);
} }

11
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@ -9,12 +9,15 @@ using Avalonia.Threading;
namespace Avalonia.Controls.Platform namespace Avalonia.Controls.Platform
{ {
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderLoop public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
{ {
public InternalPlatformThreadingInterface() public InternalPlatformThreadingInterface()
{ {
TlsCurrentThreadIsLoopThread = true; TlsCurrentThreadIsLoopThread = true;
StartTimer(DispatcherPriority.Render, new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(this, new EventArgs())); StartTimer(
DispatcherPriority.Render,
new TimeSpan(0, 0, 0, 0, 66),
() => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)));
} }
private readonly AutoResetEvent _signaled = new AutoResetEvent(false); private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
@ -105,7 +108,7 @@ namespace Avalonia.Controls.Platform
public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread;
public event Action<DispatcherPriority?> Signaled; public event Action<DispatcherPriority?> Signaled;
public event EventHandler<EventArgs> Tick; public event Action<TimeSpan> Tick;
} }
} }

8
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -6,11 +6,12 @@ using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
// TODO: Need to track position of adorned elements and move the adorner if they move. // TODO: Need to track position of adorned elements and move the adorner if they move.
public class AdornerLayer : Panel public class AdornerLayer : Panel, ICustomSimpleHitTest
{ {
public static AttachedProperty<Visual> AdornedElementProperty = public static AttachedProperty<Visual> AdornedElementProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Visual>("AdornedElement"); AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Visual>("AdornedElement");
@ -137,6 +138,11 @@ namespace Avalonia.Controls.Primitives
} }
} }
public bool HitTest(Point point)
{
return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true);
}
private class AdornedElementInfo private class AdornedElementInfo
{ {
public IDisposable Subscription { get; set; } public IDisposable Subscription { get; set; }

7
src/Avalonia.Controls/ProgressBar.cs

@ -37,7 +37,8 @@ namespace Avalonia.Controls
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate"); PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged); ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.UpdateIndicatorWhenPropChanged);
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(x => x.UpdateIndicatorWhenPropChanged);
} }
public bool IsIndeterminate public bool IsIndeterminate
@ -114,9 +115,9 @@ namespace Avalonia.Controls
} }
} }
private void ValueChanged(AvaloniaPropertyChangedEventArgs e) private void UpdateIndicatorWhenPropChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateIndicator(Bounds.Size); UpdateIndicator(Bounds.Size);
} }
} }
} }

4
src/Avalonia.Controls/TextBlock.cs

@ -6,6 +6,7 @@ using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -65,7 +66,7 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<IBrush> ForegroundProperty = public static readonly AttachedProperty<IBrush> ForegroundProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, IBrush>( AvaloniaProperty.RegisterAttached<TextBlock, Control, IBrush>(
nameof(Foreground), nameof(Foreground),
new SolidColorBrush(0xff000000), Brushes.Black,
inherits: true); inherits: true);
/// <summary> /// <summary>
@ -100,6 +101,7 @@ namespace Avalonia.Controls
{ {
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true); ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>( AffectsRender<TextBlock>(
BackgroundProperty,
ForegroundProperty, ForegroundProperty,
FontWeightProperty, FontWeightProperty,
FontSizeProperty, FontSizeProperty,

1
src/Avalonia.Controls/TopLevel.cs

@ -96,7 +96,6 @@ namespace Avalonia.Controls
_applicationLifecycle = TryGetService<IApplicationLifecycle>(dependencyResolver); _applicationLifecycle = TryGetService<IApplicationLifecycle>(dependencyResolver);
_renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver); _renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver);
var renderLoop = TryGetService<IRenderLoop>(dependencyResolver);
Renderer = impl.CreateRenderer(this); Renderer = impl.CreateRenderer(this);
impl.SetInputRoot(this); impl.SetInputRoot(this);

3
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -53,7 +53,8 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IKeyboardDevice>().ToConstant(Keyboard) .Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToConstant(instance) .Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(threading) .Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(threading) .Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(threading)
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>() .Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
.Bind<IWindowingPlatform>().ToConstant(instance) .Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>(); .Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>();

8
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -2,6 +2,9 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Views\EventsView.xaml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" /> <ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
@ -17,4 +20,9 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<ItemGroup>
<EmbeddedResource Update="Views\EventsView.xaml">
<Generator>MSBuild:Compile</Generator>
</EmbeddedResource>
</ItemGroup>
</Project> </Project>

1
src/Avalonia.Diagnostics/DevTools.xaml

@ -3,6 +3,7 @@
<TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}"> <TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/> <TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/> <TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
</TabStrip> </TabStrip>
<ContentControl Content="{Binding Content}" Grid.Row="1"/> <ContentControl Content="{Binding Content}" Grid.Row="1"/>

23
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -10,6 +10,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia namespace Avalonia
@ -28,6 +29,7 @@ namespace Avalonia.Diagnostics
public class DevTools : UserControl public class DevTools : UserControl
{ {
private static Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>(); private static Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private IDisposable _keySubscription; private IDisposable _keySubscription;
public DevTools(IControl root) public DevTools(IControl root)
@ -79,6 +81,7 @@ namespace Avalonia.Diagnostics
devToolsWindow.Closed += devTools.DevToolsClosed; devToolsWindow.Closed += devTools.DevToolsClosed;
s_open.Add(control, devToolsWindow); s_open.Add(control, devToolsWindow);
MarkAsDevTool(devToolsWindow);
devToolsWindow.Show(); devToolsWindow.Show();
} }
} }
@ -89,6 +92,7 @@ namespace Avalonia.Diagnostics
var devToolsWindow = (Window)sender; var devToolsWindow = (Window)sender;
var devTools = (DevTools)devToolsWindow.Content; var devTools = (DevTools)devToolsWindow.Content;
s_open.Remove((TopLevel)devTools.Root); s_open.Remove((TopLevel)devTools.Root);
RemoveDevTool(devToolsWindow);
_keySubscription.Dispose(); _keySubscription.Dispose();
devToolsWindow.Closed -= DevToolsClosed; devToolsWindow.Closed -= DevToolsClosed;
} }
@ -116,5 +120,24 @@ namespace Avalonia.Diagnostics
} }
} }
} }
/// <summary>
/// Marks a visual as part of the DevTools, so it can be excluded from event tracking.
/// </summary>
/// <param name="visual">The visual whose root is to be marked.</param>
public static void MarkAsDevTool(IVisual visual)
{
s_visualTreeRoots.Add(visual.GetVisualRoot());
}
public static void RemoveDevTool(IVisual visual)
{
s_visualTreeRoots.Remove(visual.GetVisualRoot());
}
public static bool BelongsToDevTool(IVisual visual)
{
return s_visualTreeRoots.Contains(visual.GetVisualRoot());
}
} }
} }

38
src/Avalonia.Diagnostics/Models/EventChainLink.cs

@ -0,0 +1,38 @@
// 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 Avalonia.Interactivity;
namespace Avalonia.Diagnostics.Models
{
internal class EventChainLink
{
public EventChainLink(object handler, bool handled, RoutingStrategies route)
{
Contract.Requires<ArgumentNullException>(handler != null);
this.Handler = handler;
this.Handled = handled;
this.Route = route;
}
public object Handler { get; }
public string HandlerName
{
get
{
if (Handler is INamed named && !string.IsNullOrEmpty(named.Name))
{
return named.Name + " (" + Handler.GetType().Name + ")";
}
return Handler.GetType().Name;
}
}
public bool Handled { get; }
public RoutingStrategies Route { get; }
}
}

5
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -14,6 +14,7 @@ namespace Avalonia.Diagnostics.ViewModels
private int _selectedTab; private int _selectedTab;
private TreePageViewModel _logicalTree; private TreePageViewModel _logicalTree;
private TreePageViewModel _visualTree; private TreePageViewModel _visualTree;
private EventsViewModel _eventsView;
private string _focusedControl; private string _focusedControl;
private string _pointerOverElement; private string _pointerOverElement;
@ -21,6 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); _visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_eventsView = new EventsViewModel(root);
UpdateFocusedControl(); UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) => KeyboardDevice.Instance.PropertyChanged += (s, e) =>
@ -57,6 +59,9 @@ namespace Avalonia.Diagnostics.ViewModels
case 1: case 1:
Content = _visualTree; Content = _visualTree;
break; break;
case 2:
Content = _eventsView;
break;
} }
RaisePropertyChanged(); RaisePropertyChanged();

61
src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs

@ -0,0 +1,61 @@
// 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.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[]
{
Button.ClickEvent,
InputElement.KeyDownEvent,
InputElement.KeyUpEvent,
InputElement.TextInputEvent,
InputElement.PointerReleasedEvent,
InputElement.PointerPressedEvent,
};
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsViewModel vm)
: base(null, type.Name)
{
this.Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
this.IsExpanded = true;
}
public override bool? IsEnabled
{
get => base.IsEnabled;
set
{
if (base.IsEnabled != value)
{
base.IsEnabled = value;
if (_updateChildren && value != null)
{
foreach (var child in Children)
{
try
{
child._updateParent = false;
child.IsEnabled = value;
}
finally
{
child._updateParent = true;
}
}
}
}
}
}
}
}

98
src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs

@ -0,0 +1,98 @@
// 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 Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
private RoutedEvent _event;
private EventsViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsViewModel vm)
: base(parent, @event.Name)
{
Contract.Requires<ArgumentNullException>(@event != null);
Contract.Requires<ArgumentNullException>(vm != null);
this._event = @event;
this._parentViewModel = vm;
}
public override bool? IsEnabled
{
get => base.IsEnabled;
set
{
if (base.IsEnabled != value)
{
base.IsEnabled = value;
UpdateTracker();
if (Parent != null && _updateParent)
{
try
{
Parent._updateChildren = false;
Parent.UpdateChecked();
}
finally
{
Parent._updateChildren = true;
}
}
}
}
}
private void UpdateTracker()
{
if (IsEnabled.GetValueOrDefault() && !_isRegistered)
{
_event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true);
_isRegistered = true;
}
}
private void HandleEvent(object sender, RoutedEventArgs e)
{
if (!_isRegistered || IsEnabled == false)
return;
if (sender is IVisual v && DevTools.BelongsToDevTool(v))
return;
var s = sender;
var handled = e.Handled;
var route = e.Route;
Action handler = delegate
{
if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e))
{
_currentEvent = new FiredEvent(e, new EventChainLink(s, handled, route));
_parentViewModel.RecordedEvents.Add(_currentEvent);
while (_parentViewModel.RecordedEvents.Count > 100)
_parentViewModel.RecordedEvents.RemoveAt(0);
}
else
{
_currentEvent.AddToChain(new EventChainLink(s, handled, route));
}
};
if (!Dispatcher.UIThread.CheckAccess())
Dispatcher.UIThread.Post(handler);
else
handler();
}
}
}

78
src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs

@ -0,0 +1,78 @@
// 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.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class EventTreeNodeBase : ViewModelBase
{
internal bool _updateChildren = true;
internal bool _updateParent = true;
private bool _isExpanded;
private bool? _isEnabled = false;
public EventTreeNodeBase(EventTreeNodeBase parent, string text)
{
this.Parent = parent;
this.Text = text;
}
public IAvaloniaReadOnlyList<EventTreeNodeBase> Children
{
get;
protected set;
}
public bool IsExpanded
{
get { return _isExpanded; }
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
}
public virtual bool? IsEnabled
{
get { return _isEnabled; }
set { RaiseAndSetIfChanged(ref _isEnabled, value); }
}
public EventTreeNodeBase Parent
{
get;
}
public string Text
{
get;
private set;
}
internal void UpdateChecked()
{
IsEnabled = GetValue();
bool? GetValue()
{
if (Children == null)
return false;
bool? value = false;
for (int i = 0; i < Children.Count; i++)
{
if (i == 0)
{
value = Children[i].IsEnabled;
continue;
}
if (value != Children[i].IsEnabled)
{
value = null;
break;
}
}
return value;
}
}
}
}

60
src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs

@ -0,0 +1,60 @@
// 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.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Interactivity;
using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsViewModel : ViewModelBase
{
private readonly IControl _root;
private FiredEvent _selectedEvent;
public EventsViewModel(IControl root)
{
this._root = root;
this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
}
public EventTreeNodeBase[] Nodes { get; }
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>();
public FiredEvent SelectedEvent
{
get => _selectedEvent;
set => RaiseAndSetIfChanged(ref _selectedEvent, value);
}
private void Clear()
{
RecordedEvents.Clear();
}
}
internal class BoolToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brushes.LightGreen : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

80
src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs

@ -0,0 +1,80 @@
// 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.Collections.ObjectModel;
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics.ViewModels
{
internal class FiredEvent : ViewModelBase
{
private RoutedEventArgs _eventArgs;
private EventChainLink _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
{
Contract.Requires<ArgumentNullException>(eventArgs != null);
Contract.Requires<ArgumentNullException>(originator != null);
this._eventArgs = eventArgs;
this.Originator = originator;
AddToChain(originator);
}
public bool IsPartOfSameEventChain(RoutedEventArgs e)
{
return e == _eventArgs;
}
public RoutedEvent Event => _eventArgs.RoutedEvent;
public bool IsHandled => HandledBy?.Handled == true;
public ObservableCollection<EventChainLink> EventChain { get; } = new ObservableCollection<EventChainLink>();
public string DisplayText
{
get
{
if (IsHandled)
{
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
}
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
}
}
public EventChainLink Originator { get; }
public EventChainLink HandledBy
{
get { return _handledBy; }
set
{
if (_handledBy != value)
{
_handledBy = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(IsHandled));
RaisePropertyChanged(nameof(DisplayText));
}
}
}
public void AddToChain(object handler, bool handled, RoutingStrategies route)
{
AddToChain(new EventChainLink(handler, handled, route));
}
public void AddToChain(EventChainLink link)
{
EventChain.Add(link);
if (HandledBy == null && link.Handled)
HandledBy = link;
}
}
}

53
src/Avalonia.Diagnostics/Views/EventsView.xaml

@ -0,0 +1,53 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels">
<UserControl.Resources>
<vm:BoolToBrushConverter x:Key="boolToBrush" />
</UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" Grid.RowSpan="2">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}">
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Grid.Column="1" />
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
<ListBox Name="eventsList" Items="{Binding RecordedEvents}" SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}" Text="{Binding DisplayText}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Height="4" Grid.Row="1" />
<DockPanel Grid.Row="2" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
<ListBox Items="{Binding SelectedEvent.EventChain}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Background="{Binding Handled, Converter={StaticResource boolToBrush}}">
<TextBlock Text="{Binding Route}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding HandlerName}" />
<TextBlock Text=" handled: " />
<TextBlock Text="{Binding Handled}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<StackPanel Orientation="Horizontal" Grid.Row="3">
<Button Content="Clear" Margin="3" Command="{Binding Clear}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

32
src/Avalonia.Diagnostics/Views/EventsView.xaml.cs

@ -0,0 +1,32 @@
// 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.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Markup.Xaml;
namespace Avalonia.Diagnostics.Views
{
public class EventsView : UserControl
{
private ListBox _events;
public EventsView()
{
this.InitializeComponent();
_events = this.FindControl<ListBox>("events");
}
private void RecordedEvents_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
_events.ScrollIntoView(_events.Items.OfType<FiredEvent>().LastOrDefault());
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

19
src/Avalonia.Styling/Styling/Style.cs

@ -111,17 +111,20 @@ namespace Avalonia.Styling
{ {
var subs = GetSubscriptions(control); var subs = GetSubscriptions(control);
foreach (var animation in Animations) if (control is Animatable animatable)
{ {
IObservable<bool> obsMatch = match.ObservableResult; foreach (var animation in Animations)
if (match.ImmediateResult == true)
{ {
obsMatch = Observable.Return(true); IObservable<bool> obsMatch = match.ObservableResult;
}
var sub = animation.Apply((Animatable)control, obsMatch); if (match.ImmediateResult == true)
subs.Add(sub); {
obsMatch = Observable.Return(true);
}
var sub = animation.Apply(animatable, null, obsMatch);
subs.Add(sub);
}
} }
foreach (var setter in Setters) foreach (var setter in Setters)

26
src/Avalonia.Visuals/Animation/RenderLoopClock.cs

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Rendering;
namespace Avalonia.Animation
{
public class RenderLoopClock : ClockBase, IRenderLoopTask, IGlobalClock
{
protected override void Stop()
{
AvaloniaLocator.Current.GetService<IRenderLoop>().Remove(this);
}
bool IRenderLoopTask.NeedsUpdate => HasSubscriptions;
void IRenderLoopTask.Render()
{
}
void IRenderLoopTask.Update(TimeSpan time)
{
Pulse(time);
}
}
}

20
src/Avalonia.Visuals/Animation/TransformAnimator.cs

@ -9,10 +9,10 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class TransformAnimator : Animator<double> public class TransformAnimator : Animator<double>
{ {
DoubleAnimator childKeyFrames; DoubleAnimator childAnimator;
/// <inheritdoc/> /// <inheritdoc/>
public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete) public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> obsMatch, Action onComplete)
{ {
var ctrl = (Visual)control; var ctrl = (Visual)control;
@ -36,15 +36,15 @@ namespace Avalonia.Animation
var renderTransformType = ctrl.RenderTransform.GetType(); var renderTransformType = ctrl.RenderTransform.GetType();
if (childKeyFrames == null) if (childAnimator == null)
{ {
InitializeChildKeyFrames(); InitializeChildAnimator();
} }
// It's a transform object so let's target that. // It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType) if (renderTransformType == Property.OwnerType)
{ {
return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete); return childAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
} }
// It's a TransformGroup and try finding the target there. // It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup)) else if (renderTransformType == typeof(TransformGroup))
@ -53,7 +53,7 @@ namespace Avalonia.Animation
{ {
if (transform.GetType() == Property.OwnerType) if (transform.GetType() == Property.OwnerType)
{ {
return childKeyFrames.Apply(animation, transform, obsMatch, onComplete); return childAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete);
} }
} }
} }
@ -73,16 +73,16 @@ namespace Avalonia.Animation
return null; return null;
} }
void InitializeChildKeyFrames() void InitializeChildAnimator()
{ {
childKeyFrames = new DoubleAnimator(); childAnimator = new DoubleAnimator();
foreach (AnimatorKeyFrame keyframe in this) foreach (AnimatorKeyFrame keyframe in this)
{ {
childKeyFrames.Add(keyframe); childAnimator.Add(keyframe);
} }
childKeyFrames.Property = Property; childAnimator.Property = Property;
} }
/// <inheritdocs/> /// <inheritdocs/>

1
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

36
src/Avalonia.Visuals/Media/Brush.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media
/// Describes how an area is painted. /// Describes how an area is painted.
/// </summary> /// </summary>
[TypeConverter(typeof(BrushConverter))] [TypeConverter(typeof(BrushConverter))]
public abstract class Brush : AvaloniaObject, IBrush public abstract class Brush : AvaloniaObject, IMutableBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Opacity"/> property. /// Defines the <see cref="Opacity"/> property.
@ -18,6 +18,9 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> OpacityProperty = public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<Brush, double>(nameof(Opacity), 1.0); AvaloniaProperty.Register<Brush, double>(nameof(Opacity), 1.0);
/// <inheritdoc/>
public event EventHandler Invalidated;
/// <summary> /// <summary>
/// Gets or sets the opacity of the brush. /// Gets or sets the opacity of the brush.
/// </summary> /// </summary>
@ -50,5 +53,36 @@ namespace Avalonia.Media
throw new FormatException($"Invalid brush string: '{s}'."); throw new FormatException($"Invalid brush string: '{s}'.");
} }
/// <inheritdoc/>
public abstract IBrush ToImmutable();
/// <summary>
/// Marks a property as affecting the brush's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Brush
{
void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
(e.Sender as T)?.RaiseInvalidated(EventArgs.Empty);
}
foreach (var property in properties)
{
property.Changed.Subscribe(Invalidate);
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
} }
} }

65
src/Avalonia.Visuals/Media/GradientBrush.cs

@ -1,7 +1,11 @@
// 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 System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using Avalonia.Collections;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Media namespace Avalonia.Media
@ -20,35 +24,74 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Defines the <see cref="GradientStops"/> property. /// Defines the <see cref="GradientStops"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IList<GradientStop>> GradientStopsProperty = public static readonly StyledProperty<GradientStops> GradientStopsProperty =
AvaloniaProperty.Register<GradientBrush, IList<GradientStop>>(nameof(GradientStops)); AvaloniaProperty.Register<GradientBrush, GradientStops>(nameof(GradientStops));
private IDisposable _gradientStopsSubscription;
static GradientBrush()
{
GradientStopsProperty.Changed.Subscribe(GradientStopsChanged);
AffectsRender<LinearGradientBrush>(SpreadMethodProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GradientBrush"/> class. /// Initializes a new instance of the <see cref="GradientBrush"/> class.
/// </summary> /// </summary>
public GradientBrush() public GradientBrush()
{ {
this.GradientStops = new List<GradientStop>(); this.GradientStops = new GradientStops();
} }
/// <summary> /// <inheritdoc/>
/// Gets or sets the brush's spread method that defines how to draw a gradient that
/// doesn't fill the bounds of the destination control.
/// </summary>
public GradientSpreadMethod SpreadMethod public GradientSpreadMethod SpreadMethod
{ {
get { return GetValue(SpreadMethodProperty); } get { return GetValue(SpreadMethodProperty); }
set { SetValue(SpreadMethodProperty, value); } set { SetValue(SpreadMethodProperty, value); }
} }
/// <summary> /// <inheritdoc/>
/// Gets or sets the brush's gradient stops.
/// </summary>
[Content] [Content]
public IList<GradientStop> GradientStops public GradientStops GradientStops
{ {
get { return GetValue(GradientStopsProperty); } get { return GetValue(GradientStopsProperty); }
set { SetValue(GradientStopsProperty, value); } set { SetValue(GradientStopsProperty, value); }
} }
/// <inheritdoc/>
IReadOnlyList<IGradientStop> IGradientBrush.GradientStops => GradientStops;
private static void GradientStopsChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is GradientBrush brush)
{
var oldValue = (GradientStops)e.OldValue;
var newValue = (GradientStops)e.NewValue;
if (oldValue != null)
{
oldValue.CollectionChanged -= brush.GradientStopsChanged;
brush._gradientStopsSubscription.Dispose();
}
if (newValue != null)
{
newValue.CollectionChanged += brush.GradientStopsChanged;
brush._gradientStopsSubscription = newValue.TrackItemPropertyChanged(brush.GradientStopChanged);
}
brush.RaiseInvalidated(EventArgs.Empty);
}
}
private void GradientStopsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaiseInvalidated(EventArgs.Empty);
}
private void GradientStopChanged(Tuple<object, PropertyChangedEventArgs> e)
{
RaiseInvalidated(EventArgs.Empty);
}
} }
} }

40
src/Avalonia.Visuals/Media/GradientStop.cs

@ -4,10 +4,22 @@
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// GradientStop /// Describes the location and color of a transition point in a gradient.
/// </summary> /// </summary>
public sealed class GradientStop public sealed class GradientStop : AvaloniaObject, IGradientStop
{ {
/// <summary>
/// Describes the <see cref="Offset"/> property.
/// </summary>
public static StyledProperty<double> OffsetProperty =
AvaloniaProperty.Register<GradientStop, double>(nameof(Offset));
/// <summary>
/// Describes the <see cref="Color"/> property.
/// </summary>
public static StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<GradientStop, Color>(nameof(Color));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GradientStop"/> class. /// Initializes a new instance of the <see cref="GradientStop"/> class.
/// </summary> /// </summary>
@ -24,16 +36,18 @@ namespace Avalonia.Media
Offset = offset; Offset = offset;
} }
// TODO: Make these dependency properties. /// <inheritdoc/>
public double Offset
/// <summary> {
/// The offset get => GetValue(OffsetProperty);
/// </summary> set => SetValue(OffsetProperty, value);
public double Offset { get; set; } }
/// <summary> /// <inheritdoc/>
/// The color public Color Color
/// </summary> {
public Color Color { get; set; } get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
} }
} }

23
src/Avalonia.Visuals/Media/GradientStops.cs

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Media.Immutable;
namespace Avalonia.Media
{
/// <summary>
/// A collection of <see cref="GradientStop"/>s.
/// </summary>
public class GradientStops : AvaloniaList<GradientStop>
{
public GradientStops()
{
ResetBehavior = ResetBehavior.Remove;
}
public IReadOnlyList<ImmutableGradientStop> ToImmutable()
{
return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToList();
}
}
}

16
src/Avalonia.Visuals/Media/IAffectsRender.cs

@ -0,0 +1,16 @@
using System;
namespace Avalonia.Media
{
/// <summary>
/// Signals to a self-rendering control that changes to the resource should invoke
/// <see cref="Visual.InvalidateVisual"/>.
/// </summary>
public interface IAffectsRender
{
/// <summary>
/// Raised when the resource changes visually.
/// </summary>
event EventHandler Invalidated;
}
}

4
src/Avalonia.Visuals/Media/IGradientBrush.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets the brush's gradient stops. /// Gets the brush's gradient stops.
/// </summary> /// </summary>
IList<GradientStop> GradientStops { get; } IReadOnlyList<IGradientStop> GradientStops { get; }
/// <summary> /// <summary>
/// Gets the brush's spread method that defines how to draw a gradient that doesn't fill /// Gets the brush's spread method that defines how to draw a gradient that doesn't fill
@ -18,4 +18,4 @@ namespace Avalonia.Media
/// </summary> /// </summary>
GradientSpreadMethod SpreadMethod { get; } GradientSpreadMethod SpreadMethod { get; }
} }
} }

18
src/Avalonia.Visuals/Media/IGradientStop.cs

@ -0,0 +1,18 @@
namespace Avalonia.Media
{
/// <summary>
/// Describes the location and color of a transition point in a gradient.
/// </summary>
public interface IGradientStop
{
/// <summary>
/// Gets the gradient stop color.
/// </summary>
Color Color { get; }
/// <summary>
/// Gets the gradient stop offset.
/// </summary>
double Offset { get; }
}
}

6
src/Avalonia.Visuals/Media/IMutableBrush.cs

@ -1,9 +1,11 @@
namespace Avalonia.Media using System;
namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Represents a mutable brush which can return an immutable clone of itself. /// Represents a mutable brush which can return an immutable clone of itself.
/// </summary> /// </summary>
public interface IMutableBrush : IBrush public interface IMutableBrush : IBrush, IAffectsRender
{ {
/// <summary> /// <summary>
/// Creates an immutable clone of the brush. /// Creates an immutable clone of the brush.

12
src/Avalonia.Visuals/Media/ImageBrush.cs

@ -2,13 +2,14 @@
// 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 Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Paints an area with an <see cref="IBitmap"/>. /// Paints an area with an <see cref="IBitmap"/>.
/// </summary> /// </summary>
public class ImageBrush : TileBrush, IImageBrush, IMutableBrush public class ImageBrush : TileBrush, IImageBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Visual"/> property. /// Defines the <see cref="Visual"/> property.
@ -16,6 +17,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<IBitmap> SourceProperty = public static readonly StyledProperty<IBitmap> SourceProperty =
AvaloniaProperty.Register<ImageBrush, IBitmap>(nameof(Source)); AvaloniaProperty.Register<ImageBrush, IBitmap>(nameof(Source));
static ImageBrush()
{
AffectsRender<ImageBrush>(SourceProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class. /// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary> /// </summary>
@ -42,9 +48,9 @@ namespace Avalonia.Media
} }
/// <inheritdoc/> /// <inheritdoc/>
IBrush IMutableBrush.ToImmutable() public override IBrush ToImmutable()
{ {
return new Immutable.ImmutableImageBrush(this); return new ImmutableImageBrush(this);
} }
} }
} }

9
src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Media.Immutable namespace Avalonia.Media.Immutable
{ {
@ -15,7 +14,7 @@ namespace Avalonia.Media.Immutable
/// <param name="opacity">The opacity of the brush.</param> /// <param name="opacity">The opacity of the brush.</param>
/// <param name="spreadMethod">The spread method.</param> /// <param name="spreadMethod">The spread method.</param>
protected ImmutableGradientBrush( protected ImmutableGradientBrush(
IList<GradientStop> gradientStops, IReadOnlyList<ImmutableGradientStop> gradientStops,
double opacity, double opacity,
GradientSpreadMethod spreadMethod) GradientSpreadMethod spreadMethod)
{ {
@ -28,14 +27,14 @@ namespace Avalonia.Media.Immutable
/// Initializes a new instance of the <see cref="ImmutableGradientBrush"/> class. /// Initializes a new instance of the <see cref="ImmutableGradientBrush"/> class.
/// </summary> /// </summary>
/// <param name="source">The brush from which this brush's properties should be copied.</param> /// <param name="source">The brush from which this brush's properties should be copied.</param>
protected ImmutableGradientBrush(IGradientBrush source) protected ImmutableGradientBrush(GradientBrush source)
: this(source.GradientStops.ToList(), source.Opacity, source.SpreadMethod) : this(source.GradientStops.ToImmutable(), source.Opacity, source.SpreadMethod)
{ {
} }
/// <inheritdoc/> /// <inheritdoc/>
public IList<GradientStop> GradientStops { get; } public IReadOnlyList<IGradientStop> GradientStops { get; }
/// <inheritdoc/> /// <inheritdoc/>
public double Opacity { get; } public double Opacity { get; }

20
src/Avalonia.Visuals/Media/Immutable/ImmutableGradientStop.cs

@ -0,0 +1,20 @@
namespace Avalonia.Media.Immutable
{
/// <summary>
/// Describes the location and color of a transition point in a gradient.
/// </summary>
public class ImmutableGradientStop : IGradientStop
{
public ImmutableGradientStop(double offset, Color color)
{
Offset = offset;
Color = color;
}
/// <inheritdoc/>
public double Offset { get; }
/// <inheritdoc/>
public Color Color { get; }
}
}

4
src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs

@ -16,7 +16,7 @@ namespace Avalonia.Media.Immutable
/// <param name="startPoint">The start point for the gradient.</param> /// <param name="startPoint">The start point for the gradient.</param>
/// <param name="endPoint">The end point for the gradient.</param> /// <param name="endPoint">The end point for the gradient.</param>
public ImmutableLinearGradientBrush( public ImmutableLinearGradientBrush(
IList<GradientStop> gradientStops, IReadOnlyList<ImmutableGradientStop> gradientStops,
double opacity = 1, double opacity = 1,
GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad,
RelativePoint? startPoint = null, RelativePoint? startPoint = null,
@ -31,7 +31,7 @@ namespace Avalonia.Media.Immutable
/// Initializes a new instance of the <see cref="ImmutableLinearGradientBrush"/> class. /// Initializes a new instance of the <see cref="ImmutableLinearGradientBrush"/> class.
/// </summary> /// </summary>
/// <param name="source">The brush from which this brush's properties should be copied.</param> /// <param name="source">The brush from which this brush's properties should be copied.</param>
public ImmutableLinearGradientBrush(ILinearGradientBrush source) public ImmutableLinearGradientBrush(LinearGradientBrush source)
: base(source) : base(source)
{ {
StartPoint = source.StartPoint; StartPoint = source.StartPoint;

4
src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs

@ -21,7 +21,7 @@ namespace Avalonia.Media.Immutable
/// The horizontal and vertical radius of the outermost circle of the radial gradient. /// The horizontal and vertical radius of the outermost circle of the radial gradient.
/// </param> /// </param>
public ImmutableRadialGradientBrush( public ImmutableRadialGradientBrush(
IList<GradientStop> gradientStops, IReadOnlyList<ImmutableGradientStop> gradientStops,
double opacity = 1, double opacity = 1,
GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad,
RelativePoint? center = null, RelativePoint? center = null,
@ -38,7 +38,7 @@ namespace Avalonia.Media.Immutable
/// Initializes a new instance of the <see cref="ImmutableRadialGradientBrush"/> class. /// Initializes a new instance of the <see cref="ImmutableRadialGradientBrush"/> class.
/// </summary> /// </summary>
/// <param name="source">The brush from which this brush's properties should be copied.</param> /// <param name="source">The brush from which this brush's properties should be copied.</param>
public ImmutableRadialGradientBrush(IRadialGradientBrush source) public ImmutableRadialGradientBrush(RadialGradientBrush source)
: base(source) : base(source)
{ {
Center = source.Center; Center = source.Center;

13
src/Avalonia.Visuals/Media/LinearGradientBrush.cs

@ -1,12 +1,14 @@
// 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 Avalonia.Media.Immutable;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// A brush that draws with a linear gradient. /// A brush that draws with a linear gradient.
/// </summary> /// </summary>
public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush, IMutableBrush public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="StartPoint"/> property. /// Defines the <see cref="StartPoint"/> property.
@ -24,6 +26,11 @@ namespace Avalonia.Media
nameof(EndPoint), nameof(EndPoint),
RelativePoint.BottomRight); RelativePoint.BottomRight);
static LinearGradientBrush()
{
AffectsRender<LinearGradientBrush>(StartPointProperty, EndPointProperty);
}
/// <summary> /// <summary>
/// Gets or sets the start point for the gradient. /// Gets or sets the start point for the gradient.
/// </summary> /// </summary>
@ -43,9 +50,9 @@ namespace Avalonia.Media
} }
/// <inheritdoc/> /// <inheritdoc/>
IBrush IMutableBrush.ToImmutable() public override IBrush ToImmutable()
{ {
return new Immutable.ImmutableLinearGradientBrush(this); return new ImmutableLinearGradientBrush(this);
} }
} }
} }

10
src/Avalonia.Visuals/Media/RadialGradientBrush.cs

@ -1,12 +1,14 @@
// 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 Avalonia.Media.Immutable;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Paints an area with a radial gradient. /// Paints an area with a radial gradient.
/// </summary> /// </summary>
public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush, IMutableBrush public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Center"/> property. /// Defines the <see cref="Center"/> property.
@ -63,9 +65,9 @@ namespace Avalonia.Media
} }
/// <inheritdoc/> /// <inheritdoc/>
IBrush IMutableBrush.ToImmutable() public override IBrush ToImmutable()
{ {
return new Immutable.ImmutableRadialGradientBrush(this); return new ImmutableRadialGradientBrush(this);
} }
} }
} }

13
src/Avalonia.Visuals/Media/SolidColorBrush.cs

@ -1,12 +1,14 @@
// 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 Avalonia.Media.Immutable;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Fills an area with a solid color. /// Fills an area with a solid color.
/// </summary> /// </summary>
public class SolidColorBrush : Brush, ISolidColorBrush, IMutableBrush public class SolidColorBrush : Brush, ISolidColorBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Color"/> property. /// Defines the <see cref="Color"/> property.
@ -14,6 +16,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<Color> ColorProperty = public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<SolidColorBrush, Color>(nameof(Color)); AvaloniaProperty.Register<SolidColorBrush, Color>(nameof(Color));
static SolidColorBrush()
{
AffectsRender<SolidColorBrush>(ColorProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SolidColorBrush"/> class. /// Initializes a new instance of the <see cref="SolidColorBrush"/> class.
/// </summary> /// </summary>
@ -75,9 +82,9 @@ namespace Avalonia.Media
} }
/// <inheritdoc/> /// <inheritdoc/>
IBrush IMutableBrush.ToImmutable() public override IBrush ToImmutable()
{ {
return new Immutable.ImmutableSolidColorBrush(this); return new ImmutableSolidColorBrush(this);
} }
} }
} }

7
src/Avalonia.Visuals/Media/TileBrush.cs

@ -79,6 +79,13 @@ namespace Avalonia.Media
static TileBrush() static TileBrush()
{ {
AffectsRender<TileBrush>(
AlignmentXProperty,
AlignmentYProperty,
DestinationRectProperty,
SourceRectProperty,
StretchProperty,
TileModeProperty);
RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue<TileBrush>(BitmapInterpolationMode.Default); RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue<TileBrush>(BitmapInterpolationMode.Default);
} }

12
src/Avalonia.Visuals/Media/VisualBrush.cs

@ -1,6 +1,7 @@
// 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 Avalonia.Media.Immutable;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Media namespace Avalonia.Media
@ -8,7 +9,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Paints an area with an <see cref="IVisual"/>. /// Paints an area with an <see cref="IVisual"/>.
/// </summary> /// </summary>
public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush public class VisualBrush : TileBrush, IVisualBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Visual"/> property. /// Defines the <see cref="Visual"/> property.
@ -16,6 +17,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<IVisual> VisualProperty = public static readonly StyledProperty<IVisual> VisualProperty =
AvaloniaProperty.Register<VisualBrush, IVisual>(nameof(Visual)); AvaloniaProperty.Register<VisualBrush, IVisual>(nameof(Visual));
static VisualBrush()
{
AffectsRender<VisualBrush>(VisualProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="VisualBrush"/> class. /// Initializes a new instance of the <see cref="VisualBrush"/> class.
/// </summary> /// </summary>
@ -42,9 +48,9 @@ namespace Avalonia.Media
} }
/// <inheritdoc/> /// <inheritdoc/>
IBrush IMutableBrush.ToImmutable() public override IBrush ToImmutable()
{ {
return new Immutable.ImmutableVisualBrush(this); return new ImmutableVisualBrush(this);
} }
} }
} }

24
src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs → src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs

@ -2,31 +2,33 @@
// 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 System;
using System.Diagnostics;
using System.Threading.Tasks;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Rendering namespace Avalonia.Rendering
{ {
/// <summary> /// <summary>
/// Defines a default render loop that uses a standard timer. /// Defines a default render timer that uses a standard timer.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This class may be overridden by platform implementations to use a specialized timer /// This class may be overridden by platform implementations to use a specialized timer
/// implementation. /// implementation.
/// </remarks> /// </remarks>
public class DefaultRenderLoop : IRenderLoop public class DefaultRenderTimer : IRenderTimer
{ {
private IRuntimePlatform _runtime; private IRuntimePlatform _runtime;
private int _subscriberCount; private int _subscriberCount;
private EventHandler<EventArgs> _tick; private Action<TimeSpan> _tick;
private IDisposable _subscription; private IDisposable _subscription;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DefaultRenderLoop"/> class. /// Initializes a new instance of the <see cref="DefaultRenderTimer"/> class.
/// </summary> /// </summary>
/// <param name="framesPerSecond"> /// <param name="framesPerSecond">
/// The number of frames per second at which the loop should run. /// The number of frames per second at which the loop should run.
/// </param> /// </param>
public DefaultRenderLoop(int framesPerSecond) public DefaultRenderTimer(int framesPerSecond)
{ {
FramesPerSecond = framesPerSecond; FramesPerSecond = framesPerSecond;
} }
@ -37,7 +39,7 @@ namespace Avalonia.Rendering
public int FramesPerSecond { get; } public int FramesPerSecond { get; }
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<EventArgs> Tick public event Action<TimeSpan> Tick
{ {
add add
{ {
@ -76,14 +78,16 @@ namespace Avalonia.Rendering
/// This can be overridden by platform implementations to use a specialized timer /// This can be overridden by platform implementations to use a specialized timer
/// implementation. /// implementation.
/// </remarks> /// </remarks>
protected virtual IDisposable StartCore(Action tick) protected virtual IDisposable StartCore(Action<TimeSpan> tick)
{ {
if (_runtime == null) if (_runtime == null)
{ {
_runtime = AvaloniaLocator.Current.GetService<IRuntimePlatform>(); _runtime = AvaloniaLocator.Current.GetService<IRuntimePlatform>();
} }
return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), tick); return _runtime.StartSystemTimer(
TimeSpan.FromSeconds(1.0 / FramesPerSecond),
() => tick(TimeSpan.FromMilliseconds(Environment.TickCount)));
} }
/// <summary> /// <summary>
@ -95,9 +99,9 @@ namespace Avalonia.Rendering
_subscription = null; _subscription = null;
} }
private void InternalTick() private void InternalTick(TimeSpan tickCount)
{ {
_tick(this, EventArgs.Empty); _tick(tickCount);
} }
} }
} }

93
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -13,6 +13,7 @@ using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using System.Threading.Tasks;
namespace Avalonia.Rendering namespace Avalonia.Rendering
{ {
@ -20,7 +21,7 @@ namespace Avalonia.Rendering
/// A renderer which renders the state of the visual tree to an intermediate scene graph /// A renderer which renders the state of the visual tree to an intermediate scene graph
/// representation which is then rendered on a rendering thread. /// representation which is then rendered on a rendering thread.
/// </summary> /// </summary>
public class DeferredRenderer : RendererBase, IRenderer, IVisualBrushRenderer public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer
{ {
private readonly IDispatcher _dispatcher; private readonly IDispatcher _dispatcher;
private readonly IRenderLoop _renderLoop; private readonly IRenderLoop _renderLoop;
@ -31,7 +32,6 @@ namespace Avalonia.Rendering
private volatile IRef<Scene> _scene; private volatile IRef<Scene> _scene;
private DirtyVisuals _dirty; private DirtyVisuals _dirty;
private IRef<IRenderTargetBitmapImpl> _overlay; private IRef<IRenderTargetBitmapImpl> _overlay;
private bool _updateQueued;
private object _rendering = new object(); private object _rendering = new object();
private int _lastSceneId = -1; private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
@ -149,7 +149,7 @@ namespace Avalonia.Rendering
{ {
if (!_running && _renderLoop != null) if (!_running && _renderLoop != null)
{ {
_renderLoop.Tick += OnRenderLoopTick; _renderLoop.Add(this);
_running = true; _running = true;
} }
} }
@ -159,11 +159,23 @@ namespace Avalonia.Rendering
{ {
if (_running && _renderLoop != null) if (_running && _renderLoop != null)
{ {
_renderLoop.Tick -= OnRenderLoopTick; _renderLoop.Remove(this);
_running = false; _running = false;
} }
} }
bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0;
void IRenderLoopTask.Update(TimeSpan time) => UpdateScene();
void IRenderLoopTask.Render()
{
using (var scene = _scene?.Clone())
{
Render(scene?.Item);
}
}
/// <inheritdoc/> /// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{ {
@ -381,67 +393,34 @@ namespace Avalonia.Rendering
private void UpdateScene() private void UpdateScene()
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
if (_root.IsVisible)
try
{ {
if (_root.IsVisible) var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
{ var scene = sceneRef.Item;
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
var scene = sceneRef.Item;
if (_dirty == null)
{
_dirty = new DirtyVisuals();
_sceneBuilder.UpdateAll(scene);
}
else if (_dirty.Count > 0)
{
foreach (var visual in _dirty)
{
_sceneBuilder.Update(scene, visual);
}
}
var oldScene = Interlocked.Exchange(ref _scene, sceneRef);
oldScene?.Dispose();
_dirty.Clear(); if (_dirty == null)
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
}
else
{ {
var oldScene = Interlocked.Exchange(ref _scene, null); _dirty = new DirtyVisuals();
oldScene?.Dispose(); _sceneBuilder.UpdateAll(scene);
} }
} else if (_dirty.Count > 0)
finally
{
_updateQueued = false;
}
}
private void OnRenderLoopTick(object sender, EventArgs e)
{
if (Monitor.TryEnter(_rendering))
{
try
{ {
if (!_updateQueued && (_dirty == null || _dirty.Count > 0)) foreach (var visual in _dirty)
{ {
_updateQueued = true; _sceneBuilder.Update(scene, visual);
_dispatcher.Post(UpdateScene, DispatcherPriority.Render);
}
using (var scene = _scene?.Clone())
{
Render(scene?.Item);
} }
} }
catch { }
finally var oldScene = Interlocked.Exchange(ref _scene, sceneRef);
{ oldScene?.Dispose();
Monitor.Exit(_rendering);
} _dirty.Clear();
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
}
else
{
var oldScene = Interlocked.Exchange(ref _scene, null);
oldScene?.Dispose();
} }
} }

12
src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs

@ -0,0 +1,12 @@
namespace Avalonia.Rendering
{
/// <summary>
/// An interface to allow non-templated controls to customize their hit-testing
/// when using a renderer with a simple hit-testing algorithm without a scene graph,
/// such as <see cref="ImmediateRenderer" />
/// </summary>
public interface ICustomSimpleHitTest
{
bool HitTest(Point point);
}
}

27
src/Avalonia.Visuals/Rendering/IRenderLoop.cs

@ -1,19 +1,28 @@
using System; namespace Avalonia.Rendering
namespace Avalonia.Rendering
{ {
/// <summary> /// <summary>
/// Defines the interface implemented by an application render loop. /// The application render loop.
/// </summary> /// </summary>
/// <remarks>
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// </remarks>
public interface IRenderLoop public interface IRenderLoop
{ {
/// <summary> /// <summary>
/// Raised when the render loop ticks to signal a new frame should be drawn. /// Adds an update task.
/// </summary> /// </summary>
/// <param name="i">The update task.</param>
/// <remarks> /// <remarks>
/// This event can be raised on any thread; it is the responsibility of the subscriber to /// Registered update tasks will be polled on each tick of the render loop after the
/// switch execution to the right thread. /// animation timer has been pulsed.
/// </remarks> /// </remarks>
event EventHandler<EventArgs> Tick; void Add(IRenderLoopTask i);
/// <summary>
/// Removes an update task.
/// </summary>
/// <param name="i">The update task.</param>
void Remove(IRenderLoopTask i);
} }
} }

12
src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.Rendering
{
public interface IRenderLoopTask
{
bool NeedsUpdate { get; }
void Update(TimeSpan time);
void Render();
}
}

20
src/Avalonia.Visuals/Rendering/IRenderTimer.cs

@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.Rendering
{
/// <summary>
/// Defines the interface implemented by an application render timer.
/// </summary>
public interface IRenderTimer
{
/// <summary>
/// Raised when the render timer ticks to signal a new frame should be drawn.
/// </summary>
/// <remarks>
/// This event can be raised on any thread; it is the responsibility of the subscriber to
/// switch execution to the right thread.
/// </remarks>
event Action<TimeSpan> Tick;
}
}

11
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -200,7 +200,16 @@ namespace Avalonia.Rendering
if (filter?.Invoke(visual) != false) if (filter?.Invoke(visual) != false)
{ {
bool containsPoint = visual.TransformedBounds?.Contains(p) == true; bool containsPoint = false;
if (visual is ICustomSimpleHitTest custom)
{
containsPoint = custom.HitTest(p);
}
else
{
containsPoint = visual.TransformedBounds?.Contains(p) == true;
}
if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0) if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0)
{ {

120
src/Avalonia.Visuals/Rendering/RenderLoop.cs

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Avalonia.Logging;
using Avalonia.Threading;
namespace Avalonia.Rendering
{
/// <summary>
/// The application render loop.
/// </summary>
/// <remarks>
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// </remarks>
public class RenderLoop : IRenderLoop
{
private readonly IDispatcher _dispatcher;
private List<IRenderLoopTask> _items = new List<IRenderLoopTask>();
private IRenderTimer _timer;
private int inTick;
/// <summary>
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
/// </summary>
public RenderLoop()
{
_dispatcher = Dispatcher.UIThread;
}
/// <summary>
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
/// </summary>
/// <param name="timer">The render timer.</param>
/// <param name="dispatcher">The UI thread dispatcher.</param>
public RenderLoop(IRenderTimer timer, IDispatcher dispatcher)
{
_timer = timer;
_dispatcher = dispatcher;
}
/// <summary>
/// Gets the render timer.
/// </summary>
protected IRenderTimer Timer
{
get
{
if (_timer == null)
{
_timer = AvaloniaLocator.Current.GetService<IRenderTimer>();
}
return _timer;
}
}
/// <inheritdoc/>
public void Add(IRenderLoopTask i)
{
Contract.Requires<ArgumentNullException>(i != null);
Dispatcher.UIThread.VerifyAccess();
_items.Add(i);
if (_items.Count == 1)
{
Timer.Tick += TimerTick;
}
}
/// <inheritdoc/>
public void Remove(IRenderLoopTask i)
{
Contract.Requires<ArgumentNullException>(i != null);
Dispatcher.UIThread.VerifyAccess();
_items.Remove(i);
if (_items.Count == 0)
{
Timer.Tick -= TimerTick;
}
}
private async void TimerTick(TimeSpan time)
{
if (Interlocked.CompareExchange(ref inTick, 1, 0) == 0)
{
try
{
if (_items.Any(item => item.NeedsUpdate))
{
await _dispatcher.InvokeAsync(() =>
{
foreach (var i in _items)
{
i.Update(time);
}
}, DispatcherPriority.Render).ConfigureAwait(false);
}
foreach (var i in _items)
{
i.Render();
}
}
catch (Exception ex)
{
Logger.Error(LogArea.Visual, this, "Exception in render loop: {Error}", ex);
}
finally
{
Interlocked.Exchange(ref inTick, 0);
}
}
}
}
}

19
src/Avalonia.Visuals/Visual.cs

@ -338,11 +338,24 @@ namespace Avalonia
/// FrameworkPropertyMetadata.AffectsRender flag. /// FrameworkPropertyMetadata.AffectsRender flag.
/// </remarks> /// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties) protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : class, IVisual where T : Visual
{ {
void Invalidate(AvaloniaPropertyChangedEventArgs e) void Invalidate(AvaloniaPropertyChangedEventArgs e)
{ {
(e.Sender as T)?.InvalidateVisual(); if (e.Sender is T sender)
{
if (e.OldValue is IAffectsRender oldValue)
{
oldValue.Invalidated -= sender.AffectsRenderInvalidated;
}
if (e.NewValue is IAffectsRender newValue)
{
newValue.Invalidated += sender.AffectsRenderInvalidated;
}
sender.InvalidateVisual();
}
} }
foreach (var property in properties) foreach (var property in properties)
@ -544,6 +557,8 @@ namespace Avalonia
OnVisualParentChanged(old, value); OnVisualParentChanged(old, value);
} }
private void AffectsRenderInvalidated(object sender, EventArgs e) => InvalidateVisual();
/// <summary> /// <summary>
/// Called when the <see cref="VisualChildren"/> collection changes. /// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary> /// </summary>

3
src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs

@ -52,7 +52,8 @@ namespace Avalonia.Gtk3
.Bind<IPlatformSettings>().ToConstant(Instance) .Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(Instance) .Bind<IPlatformThreadingInterface>().ToConstant(Instance)
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialog>() .Bind<ISystemDialogImpl>().ToSingleton<SystemDialog>()
.Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60)) .Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoader()); .Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoader());
} }

3
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -35,7 +35,8 @@ namespace Avalonia.LinuxFramebuffer
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice) .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(Threading) .Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderLoop>().ToConstant(Threading); .Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(Threading);
} }
internal static TopLevel Initialize<T>(T builder, string fbdev = null) where T : AppBuilderBase<T>, new() internal static TopLevel Initialize<T>(T builder, string fbdev = null) where T : AppBuilderBase<T>, new()

7
src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs

@ -21,6 +21,7 @@ namespace Avalonia.MonoMac
private static bool s_monoMacInitialized; private static bool s_monoMacInitialized;
private static bool s_showInDock = true; private static bool s_showInDock = true;
private static IRenderLoop s_renderLoop; private static IRenderLoop s_renderLoop;
private static IRenderTimer s_renderTimer;
void DoInitialize() void DoInitialize()
{ {
@ -35,6 +36,7 @@ namespace Avalonia.MonoMac
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsImpl>() .Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsImpl>()
.Bind<IClipboard>().ToSingleton<ClipboardImpl>() .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<IRenderLoop>().ToConstant(s_renderLoop) .Bind<IRenderLoop>().ToConstant(s_renderLoop)
.Bind<IRenderTimer>().ToConstant(s_renderTimer)
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance) .Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
/*.Bind<IPlatformDragSource>().ToTransient<DragSource>()*/; /*.Bind<IPlatformDragSource>().ToTransient<DragSource>()*/;
} }
@ -83,7 +85,8 @@ namespace Avalonia.MonoMac
ThreadHelper.InitializeCocoaThreadingLocks(); ThreadHelper.InitializeCocoaThreadingLocks();
App = NSApplication.SharedApplication; App = NSApplication.SharedApplication;
UpdateActivationPolicy(); UpdateActivationPolicy();
s_renderLoop = new RenderLoop(); //TODO: use CVDisplayLink s_renderLoop = new RenderLoop();
s_renderTimer = new RenderTimer(60); //TODO: use CVDisplayLink
s_monoMacInitialized = true; s_monoMacInitialized = true;
} }
@ -133,4 +136,4 @@ namespace Avalonia
return builder.UseWindowingSubsystem(MonoMac.MonoMacPlatform.Initialize, "MonoMac"); return builder.UseWindowingSubsystem(MonoMac.MonoMacPlatform.Initialize, "MonoMac");
} }
} }
} }

31
src/OSX/Avalonia.MonoMac/RenderLoop.cs

@ -1,31 +0,0 @@
using System;
using Avalonia.Platform;
using Avalonia.Rendering;
using MonoMac.Foundation;
namespace Avalonia.MonoMac
{
//TODO: Switch to using CVDisplayLink
public class RenderLoop : IRenderLoop
{
private readonly object _lock = new object();
private readonly IDisposable _timer;
public RenderLoop()
{
_timer = AvaloniaLocator.Current.GetService<IRuntimePlatform>().StartSystemTimer(new TimeSpan(0, 0, 0, 0, 1000 / 60),
() =>
{
lock (_lock)
{
using (new NSAutoreleasePool())
{
Tick?.Invoke(this, EventArgs.Empty);
}
}
});
}
public event EventHandler<EventArgs> Tick;
}
}

28
src/OSX/Avalonia.MonoMac/RenderTimer.cs

@ -0,0 +1,28 @@
using System;
using Avalonia.Platform;
using Avalonia.Rendering;
using MonoMac.Foundation;
namespace Avalonia.MonoMac
{
//TODO: Switch to using CVDisplayLink
public class RenderTimer : DefaultRenderTimer
{
public RenderTimer(int framesPerSecond) : base(framesPerSecond)
{
}
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
return AvaloniaLocator.Current.GetService<IRuntimePlatform>().StartSystemTimer(
TimeSpan.FromSeconds(1.0 / FramesPerSecond),
() =>
{
using (new NSAutoreleasePool())
{
tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
}
});
}
}
}

25
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -26,6 +26,9 @@ namespace Avalonia.Win32.Interop
public delegate void TimeCallback(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2); public delegate void TimeCallback(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void WaitOrTimerCallback(IntPtr lpParameter, bool timerOrWaitFired);
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
public enum Cursor public enum Cursor
@ -848,11 +851,25 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow);
[DllImport("Winmm.dll")] [DllImport("kernel32.dll", SetLastError = true)]
public static extern uint timeKillEvent(uint uTimerID); public static extern IntPtr CreateTimerQueue();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeleteTimerQueueEx(IntPtr TimerQueue, IntPtr CompletionEvent);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CreateTimerQueueTimer(
out IntPtr phNewTimer,
IntPtr TimerQueue,
WaitOrTimerCallback Callback,
IntPtr Parameter,
uint DueTime,
uint Period,
uint Flags);
[DllImport("Winmm.dll")] [DllImport("kernel32.dll", SetLastError = true)]
public static extern uint timeSetEvent(uint uDelay, uint uResolution, TimeCallback lpTimeProc, UIntPtr dwUser, uint fuEvent); public static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern int ToUnicode( public static extern int ToUnicode(

35
src/Windows/Avalonia.Win32/RenderLoop.cs

@ -1,35 +0,0 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Rendering;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
internal class RenderLoop : DefaultRenderLoop
{
private UnmanagedMethods.TimeCallback timerDelegate;
public RenderLoop(int framesPerSecond)
: base(framesPerSecond)
{
}
protected override IDisposable StartCore(Action tick)
{
timerDelegate = (id, uMsg, user, dw1, dw2) => tick();
var handle = UnmanagedMethods.timeSetEvent(
(uint)(1000 / FramesPerSecond),
0,
timerDelegate,
UIntPtr.Zero,
1);
return Disposable.Create(() =>
{
timerDelegate = null;
UnmanagedMethods.timeKillEvent(handle);
});
}
}
}

56
src/Windows/Avalonia.Win32/RenderTimer.cs

@ -0,0 +1,56 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Rendering;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
internal class RenderTimer : DefaultRenderTimer
{
private UnmanagedMethods.WaitOrTimerCallback timerDelegate;
private static IntPtr _timerQueue;
private static void EnsureTimerQueueCreated()
{
if (Volatile.Read(ref _timerQueue) == null)
{
var queue = UnmanagedMethods.CreateTimerQueue();
if (Interlocked.CompareExchange(ref _timerQueue, queue, IntPtr.Zero) != IntPtr.Zero)
{
UnmanagedMethods.DeleteTimerQueueEx(queue, IntPtr.Zero);
}
}
}
public RenderTimer(int framesPerSecond)
: base(framesPerSecond)
{
}
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
EnsureTimerQueueCreated();
var msPerFrame = 1000 / FramesPerSecond;
timerDelegate = (_, __) => tick(TimeSpan.FromMilliseconds(Environment.TickCount));
UnmanagedMethods.CreateTimerQueueTimer(
out var timer,
_timerQueue,
timerDelegate,
IntPtr.Zero,
(uint)msPerFrame,
(uint)msPerFrame,
0
);
return Disposable.Create(() =>
{
timerDelegate = null;
UnmanagedMethods.DeleteTimerQueueTimer(_timerQueue, timer, IntPtr.Zero);
});
}
}
}

4
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -9,6 +9,7 @@ using System.IO;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
@ -82,7 +83,8 @@ namespace Avalonia.Win32
.Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance) .Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
.Bind<IPlatformSettings>().ToConstant(s_instance) .Bind<IPlatformSettings>().ToConstant(s_instance)
.Bind<IPlatformThreadingInterface>().ToConstant(s_instance) .Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
.Bind<IRenderLoop>().ToConstant(new RenderLoop(60)) .Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new RenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>() .Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(s_instance) .Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<IPlatformIconLoader>().ToConstant(s_instance); .Bind<IPlatformIconLoader>().ToConstant(s_instance);

9
src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs → src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs

@ -5,11 +5,12 @@ using Foundation;
namespace Avalonia.iOS namespace Avalonia.iOS
{ {
class DisplayLinkRenderLoop : IRenderLoop class DisplayLinkRenderTimer : IRenderTimer
{ {
public event EventHandler<EventArgs> Tick; public event Action<TimeSpan> Tick;
private CADisplayLink _link; private CADisplayLink _link;
public DisplayLinkRenderLoop()
public DisplayLinkRenderTimer()
{ {
_link = CADisplayLink.Create(OnFrame); _link = CADisplayLink.Create(OnFrame);
@ -20,7 +21,7 @@ namespace Avalonia.iOS
{ {
try try
{ {
Tick?.Invoke(this, new EventArgs()); Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
} }
catch (Exception) catch (Exception)
{ {

3
src/iOS/Avalonia.iOS/iOSPlatform.cs

@ -41,7 +41,8 @@ namespace Avalonia.iOS
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance) .Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IWindowingPlatform>().ToSingleton<WindowingPlatformImpl>() .Bind<IWindowingPlatform>().ToSingleton<WindowingPlatformImpl>()
.Bind<IRenderLoop>().ToSingleton<DisplayLinkRenderLoop>(); .Bind<IRenderTimer>().ToSingleton<DisplayLinkRenderTimer>()
.Bind<IRenderLoop>().ToSingleton<RenderLoop>();
} }
} }
} }

14
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -479,6 +479,18 @@ namespace Avalonia.Base.UnitTests
Assert.False(source.SetterCalled); Assert.False(source.SetterCalled);
} }
[Fact]
public void Disposing_Completed_Binding_Does_Not_Throw()
{
var target = new Class1();
var source = new Subject<string>();
var subscription = target.Bind(Class1.FooProperty, source);
source.OnCompleted();
subscription.Dispose();
}
/// <summary> /// <summary>
/// Returns an observable that returns a single value but does not complete. /// Returns an observable that returns a single value but does not complete.
/// </summary> /// </summary>
@ -595,4 +607,4 @@ namespace Avalonia.Base.UnitTests
public bool SetterCalled { get; private set; } public bool SetterCalled { get; private set; }
} }
} }
} }

21
tests/Avalonia.Controls.UnitTests/BorderTests.cs

@ -1,6 +1,10 @@
// 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 Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
@ -42,5 +46,22 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(6, 6, 0, 0), content.Bounds); Assert.Equal(new Rect(6, 6, 0, 0), content.Bounds);
} }
[Fact]
public void Changing_Background_Brush_Color_Should_Invalidate_Visual()
{
var target = new Border()
{
Background = new SolidColorBrush(Colors.Red),
};
var root = new TestRoot(target);
var renderer = Mock.Get(root.Renderer);
renderer.ResetCalls();
((SolidColorBrush)target.Background).Color = Colors.Green;
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
} }
} }

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

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class MenuItemTests
{
[Fact]
public void Header_Of_Minus_Should_Apply_Separator_Pseudoclass()
{
var target = new MenuItem { Header = "-" };
Assert.True(target.Classes.Contains(":separator"));
}
[Fact]
public void Separator_Item_Should_Set_Focusable_False()
{
var target = new MenuItem { Header = "-" };
Assert.False(target.Focusable);
}
}
}

21
tests/Avalonia.Controls.UnitTests/PanelTests.cs

@ -3,7 +3,11 @@
using System.Linq; using System.Linq;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
@ -115,5 +119,22 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { child2, child1 }, panel.GetLogicalChildren()); Assert.Equal(new[] { child2, child1 }, panel.GetLogicalChildren());
Assert.Equal(new[] { child2, child1 }, panel.GetVisualChildren()); Assert.Equal(new[] { child2, child1 }, panel.GetVisualChildren());
} }
[Fact]
public void Changing_Background_Brush_Color_Should_Invalidate_Visual()
{
var target = new Panel()
{
Background = new SolidColorBrush(Colors.Red),
};
var root = new TestRoot(target);
var renderer = Mock.Get(root.Renderer);
renderer.ResetCalls();
((SolidColorBrush)target.Background).Color = Colors.Green;
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
} }
} }

20
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs

@ -13,6 +13,7 @@ using System;
using System.Linq; using System.Linq;
using Xunit; using Xunit;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Media;
namespace Avalonia.Controls.UnitTests.Presenters namespace Avalonia.Controls.UnitTests.Presenters
{ {
@ -203,5 +204,22 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.NotEqual(foo, logicalChildren.First()); Assert.NotEqual(foo, logicalChildren.First());
} }
[Fact]
public void Changing_Background_Brush_Color_Should_Invalidate_Visual()
{
var target = new ContentPresenter()
{
Background = new SolidColorBrush(Colors.Red),
};
var root = new TestRoot(target);
var renderer = Mock.Get(root.Renderer);
renderer.ResetCalls();
((SolidColorBrush)target.Background).Color = Colors.Green;
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save