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>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False">
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<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>
<WrapPanel ClipToBounds="False">
<Border Classes="Test Rect1" Background="DarkRed"/>
@ -120,4 +123,4 @@
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>
</UserControl>

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

@ -5,6 +5,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using RenderDemo.ViewModels;
@ -23,5 +24,20 @@ namespace RenderDemo.Pages
{
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
{
private string _playStateText = "Pause all animations";
private bool _isPlaying = true;
public AnimationsPageViewModel()
{
ToggleGlobalPlayState = ReactiveCommand.Create(() => TogglePlayState());
}
private string _playStateText = "Pause animations on this page";
void TogglePlayState()
public void TogglePlayState()
{
switch (Animation.GlobalPlayState)
{
case PlayState.Run:
PlayStateText = "Resume all animations";
Animation.GlobalPlayState = PlayState.Pause;
break;
case PlayState.Pause:
PlayStateText = "Pause all animations";
Animation.GlobalPlayState = PlayState.Run;
break;
}
PlayStateText = _isPlaying
? "Resume animations on this page" : "Pause animations on this page";
_isPlaying = !_isPlaying;
}
public string PlayStateText
@ -34,7 +22,5 @@ namespace RenderDemo.ViewModels
get { return _playStateText; }
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<IWindowingPlatform>().ToConstant(Instance)
.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));
SkiaPlatform.Initialize();

28
src/Avalonia.Animation/Animatable.cs

@ -14,26 +14,14 @@ namespace Avalonia.Animation
/// Base class for all animatable objects.
/// </summary>
public class Animatable : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="PlayState"/> property.
/// </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;
{
public static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
/// <summary>
/// Gets or sets the state of the animation for this
/// control.
/// </summary>
public PlayState PlayState
public IClock Clock
{
get { return _playState; }
set { SetAndRaise(PlayStateProperty, ref _playState, value); }
get => GetValue(ClockProperty);
set => SetValue(ClockProperty, value);
}
/// <summary>
@ -69,9 +57,9 @@ namespace Avalonia.Animation
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>
public class Animation : AvaloniaList<KeyFrame>, IAnimation
{
/// <summary>
/// Gets or sets the animation play state for all animations
/// </summary>
public static PlayState GlobalPlayState { get; set; } = PlayState.Run;
/// <summary>
/// Gets or sets the active time of this animation.
/// </summary>
@ -149,12 +144,12 @@ namespace Avalonia.Animation
}
/// <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);
if (animators.Count == 1)
{
subscriptions.Add(animators[0].Apply(this, control, match, onComplete));
subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete));
}
else
{
@ -168,7 +163,7 @@ namespace Avalonia.Animation
animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task);
}
subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete));
subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete));
}
if (onComplete != null)
@ -180,7 +175,7 @@ namespace Avalonia.Animation
}
/// <inheritdocs/>
public Task RunAsync(Animatable control)
public Task RunAsync(Animatable control, IClock clock = null)
{
var run = new TaskCompletionSource<object>();
@ -188,7 +183,7 @@ namespace Avalonia.Animation
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null;
subscriptions = this.Apply(control, Observable.Return(true), () =>
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
run.SetResult(null);
subscriptions?.Dispose();

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

@ -8,7 +8,7 @@ using Avalonia.Reactive;
namespace Avalonia.Animation
{
/// <summary>
/// Handles interpolatoin and time-related functions
/// Handles interpolation and time-related functions
/// for keyframe animations.
/// </summary>
internal class AnimationInstance<T> : SingleSubscriberObservableBase<T>
@ -19,7 +19,6 @@ namespace Avalonia.Animation
private double _currentIteration;
private bool _isLooping;
private bool _gotFirstKFValue;
private bool _gotFirstFrameCount;
private bool _iterationDelay;
private FillMode _fillMode;
private PlaybackDirection _animationDirection;
@ -29,15 +28,14 @@ namespace Avalonia.Animation
private double _speedRatio;
private TimeSpan _delay;
private TimeSpan _duration;
private TimeSpan _firstFrameCount;
private TimeSpan _internalClock;
private TimeSpan? _previousClock;
private Easings.Easing _easeFunc;
private Action _onCompleteAction;
private Func<double, T, T> _interpolator;
private IDisposable _timerSubscription;
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)
throw new InvalidOperationException("Speed ratio cannot be negative or zero.");
@ -73,17 +71,19 @@ namespace Avalonia.Animation
_fillMode = animation.FillMode;
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
}
protected override void Unsubscribed()
{
_timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
}
protected override void Subscribed()
{
_timerSubscription = Timing.AnimationsTimer
.Subscribe(p => this.Step(p));
_clock = new Clock(_baseClock);
_timerSubscription = _clock.Subscribe(Step);
}
public void Step(TimeSpan frameTick)
@ -116,46 +116,21 @@ namespace Avalonia.Animation
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();
if (!_previousClock.HasValue)
{
_previousClock = systemTime;
_internalClock = TimeSpan.Zero;
}
else
{
if (Animation.GlobalPlayState == PlayState.Pause || _targetControl.PlayState == PlayState.Pause)
{
_previousClock = systemTime;
return;
}
var delta = systemTime - _previousClock;
_internalClock += delta.Value;
_previousClock = systemTime;
}
if (!_gotFirstKFValue)
{
_firstKFValue = (T)_parent.First().Value;
_gotFirstKFValue = true;
}
if (!_gotFirstFrameCount)
{
_firstFrameCount = _internalClock;
_gotFirstFrameCount = true;
}
}
private void InternalStep(TimeSpan systemTime)
private void InternalStep(TimeSpan time)
{
DoPlayStatesAndTime(systemTime);
var time = _internalClock - _firstFrameCount;
DoPlayStates();
var delayEndpoint = _delay;
var iterationEndpoint = delayEndpoint + _duration;
@ -176,22 +151,18 @@ namespace Avalonia.Animation
}
//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
{
_previousClock = systemTime;
return;
}
time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks);
time = TimeSpan.FromTicks((long)(time.Ticks % iterationEndpoint.Ticks));
if (!_isLooping)
{
if (_currentIteration > _repeatCount)
DoComplete();
if (time > iterationEndpoint)
if ((_currentIteration > _repeatCount) || (time > iterationEndpoint))
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.Linq;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
@ -17,7 +21,7 @@ namespace Avalonia.Animation
/// List of type-converted keyframes.
/// </summary>
private readonly List<AnimatorKeyFrame> _convertedKeyframes = new List<AnimatorKeyFrame>();
private bool _isVerifiedAndConverted;
/// <summary>
@ -28,21 +32,17 @@ namespace Avalonia.Animation
public Animator()
{
// Invalidate keyframes when changed.
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> match, Action onComplete)
public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
{
if (!_isVerifiedAndConverted)
if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames();
return match
.Where(p => p)
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control, onComplete);
});
var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete);
return match.Subscribe(subject);
}
/// <summary>
@ -52,58 +52,84 @@ namespace Avalonia.Animation
/// (i.e., the normalized time between the selected keyframes, relative to the
/// time parameter).
/// </summary>
/// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
/// <param name="animationTime">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double animationTime)
{
AnimatorKeyFrame firstCue, lastCue ;
AnimatorKeyFrame firstKeyframe, lastKeyframe;
int kvCount = _convertedKeyframes.Count;
if (kvCount > 2)
{
if (t <= 0.0)
if (animationTime <= 0.0)
{
firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1];
firstKeyframe = _convertedKeyframes[0];
lastKeyframe = _convertedKeyframes[1];
}
else if (t >= 1.0)
else if (animationTime >= 1.0)
{
firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1];
firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1];
}
else
{
(double time, int index) maxval = (0.0d, 0);
for (int i = 0; i < _convertedKeyframes.Count; i++)
{
var comp = _convertedKeyframes[i].Cue.CueValue;
if (t >= comp)
{
maxval = (comp, i);
}
}
firstCue = _convertedKeyframes[maxval.index];
lastCue = _convertedKeyframes[maxval.index + 1];
int index = FindClosestBeforeKeyFrame(animationTime);
firstKeyframe = _convertedKeyframes[index];
lastKeyframe = _convertedKeyframes[index + 1];
}
}
else
{
firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1];
firstKeyframe = _convertedKeyframes[0];
lastKeyframe = _convertedKeyframes[1];
}
double t0 = firstCue.Cue.CueValue;
double t1 = lastCue.Cue.CueValue;
var intraframeTime = (t - t0) / (t1 - t0);
var firstFrameData = (firstCue.GetTypedValue<T>(), firstCue.isNeutral);
var lastFrameData = (lastCue.GetTypedValue<T>(), lastCue.isNeutral);
double t0 = firstKeyframe.Cue.CueValue;
double t1 = lastKeyframe.Cue.CueValue;
var intraframeTime = (animationTime - t0) / (t1 - t0);
var firstFrameData = (firstKeyframe.GetTypedValue<T>(), firstKeyframe.isNeutral);
var lastFrameData = (lastKeyframe.GetTypedValue<T>(), lastKeyframe.isNeutral);
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>
/// Runs the KeyFrames Animation.
/// </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);
}
@ -124,14 +150,6 @@ namespace Avalonia.Animation
AddNeutralKeyFramesIfNeeded();
var copy = _convertedKeyframes.ToList().OrderBy(p => p.Cue.CueValue);
_convertedKeyframes.Clear();
foreach (AnimatorKeyFrame keyframe in copy)
{
_convertedKeyframes.Add(keyframe);
}
_isVerifiedAndConverted = true;
}
@ -161,7 +179,7 @@ namespace Avalonia.Animation
{
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)
@ -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>
/// 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
{

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.Threading.Tasks;
@ -9,13 +12,13 @@ namespace Avalonia.Animation
public interface IAnimation
{
/// <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>
IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete = null);
IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete = null);
/// <summary>
/// Run the animation to the specified control
/// Run the animation on the specified control.
/// </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
{
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;
namespace Avalonia.Animation
@ -16,6 +19,6 @@ namespace Avalonia.Animation
/// <summary>
/// Applies the current KeyFrame group to the specified control.
/// </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>
/// Applies the transition to the specified <see cref="Animatable"/>.
/// </summary>
IDisposable Apply(Animatable control, object oldValue, object newValue);
IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue);
/// <summary>
/// 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 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
{
/// <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>
/// 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>
/// 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>
internal class TransitionInstance : SingleSubscriberObservableBase<double>
{
private IDisposable timerSubscription;
private TimeSpan startTime;
private TimeSpan duration;
private IDisposable _timerSubscription;
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)
{
var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks;
var interpVal = (double)t.Ticks / _duration.Ticks;
if (interpVal > 1d
|| interpVal < 0d)
if (interpVal > 1d || interpVal < 0d)
{
PublishCompleted();
return;
@ -40,15 +41,15 @@ namespace Avalonia.Animation
protected override void Unsubscribed()
{
timerSubscription?.Dispose();
_timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
}
protected override void Subscribed()
{
startTime = Timing.GetTickCount();
timerSubscription = Timing.AnimationsTimer
.Subscribe(t => TimerTick(t));
_clock = new Clock(_baseClock);
_timerSubscription = _clock.Subscribe(TimerTick);
PublishNext(0.0d);
}
}
}
}

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

@ -14,7 +14,6 @@ namespace Avalonia.Animation
public abstract class Transition<T> : AvaloniaObject, ITransition
{
private AvaloniaProperty _prop;
private Easing _easing;
/// <summary>
/// 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);
/// <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);
}
}
}

7
src/Avalonia.Base/PriorityBindingEntry.cs

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

14
src/Avalonia.Base/PriorityLevel.cs

@ -112,12 +112,16 @@ namespace Avalonia
return Disposable.Create(() =>
{
Bindings.Remove(node);
entry.Dispose();
if (entry.Index >= ActiveBindingIndex)
if (!entry.HasCompleted)
{
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)
{
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);
}
private class SingleValueImpl<T> : IObservable<T>
{
private T _value;
@ -30,7 +30,6 @@ namespace Avalonia.Reactive
{
_value = value;
}
public IDisposable Subscribe(IObserver<T> observer)
{
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;
Instance.RegisterServices();
RuntimePlatformServicesInitializer();
WindowingSubsystemInitializer();
RenderingSubsystemInitializer();
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);
}

7
src/Avalonia.Controls/Application.cs

@ -4,12 +4,14 @@
using System;
using System.Reactive.Concurrency;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Threading;
@ -335,6 +337,11 @@ namespace Avalonia
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance)
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance)
.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>
static Border()
{
AffectsRender<Border>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsRender<Border>(
BackgroundProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}

20
src/Avalonia.Controls/MenuItem.cs

@ -99,13 +99,13 @@ namespace Avalonia.Controls
SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue<MenuItem>(true);
HeaderProperty.Changed.AddClassHandler<MenuItem>(x => x.HeaderChanged);
IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged);
IsSelectedProperty.Changed.AddClassHandler<MenuItem>(x => x.IsSelectedChanged);
ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel);
ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick);
SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened);
IsSubMenuOpenProperty.Changed.AddClassHandler<MenuItem>(x => x.SubMenuOpenChanged);
PseudoClass<MenuItem, object>(HeaderProperty, x => x as string == "-", ":separator");
}
public MenuItem()
@ -420,6 +420,24 @@ namespace Avalonia.Controls
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>
/// Called when the <see cref="Icon"/> property changes.
/// </summary>

1
src/Avalonia.Controls/Panel.cs

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

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

@ -9,12 +9,15 @@ using Avalonia.Threading;
namespace Avalonia.Controls.Platform
{
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderLoop
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
{
public InternalPlatformThreadingInterface()
{
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);
@ -105,7 +108,7 @@ namespace Avalonia.Controls.Platform
public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread;
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 Avalonia.VisualTree;
using Avalonia.Media;
using Avalonia.Rendering;
namespace Avalonia.Controls.Primitives
{
// 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 =
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
{
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>(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
@ -114,9 +115,9 @@ namespace Avalonia.Controls
}
}
private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
private void UpdateIndicatorWhenPropChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateIndicator(Bounds.Size);
}
}
}
}

4
src/Avalonia.Controls/TextBlock.cs

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

1
src/Avalonia.Controls/TopLevel.cs

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

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

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

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

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

1
src/Avalonia.Diagnostics/DevTools.xaml

@ -3,6 +3,7 @@
<TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
</TabStrip>
<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.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia
@ -28,6 +29,7 @@ namespace Avalonia.Diagnostics
public class DevTools : UserControl
{
private static Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private IDisposable _keySubscription;
public DevTools(IControl root)
@ -79,6 +81,7 @@ namespace Avalonia.Diagnostics
devToolsWindow.Closed += devTools.DevToolsClosed;
s_open.Add(control, devToolsWindow);
MarkAsDevTool(devToolsWindow);
devToolsWindow.Show();
}
}
@ -89,6 +92,7 @@ namespace Avalonia.Diagnostics
var devToolsWindow = (Window)sender;
var devTools = (DevTools)devToolsWindow.Content;
s_open.Remove((TopLevel)devTools.Root);
RemoveDevTool(devToolsWindow);
_keySubscription.Dispose();
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 TreePageViewModel _logicalTree;
private TreePageViewModel _visualTree;
private EventsViewModel _eventsView;
private string _focusedControl;
private string _pointerOverElement;
@ -21,6 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_eventsView = new EventsViewModel(root);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
@ -57,6 +59,9 @@ namespace Avalonia.Diagnostics.ViewModels
case 1:
Content = _visualTree;
break;
case 2:
Content = _eventsView;
break;
}
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);
foreach (var animation in Animations)
if (control is Animatable animatable)
{
IObservable<bool> obsMatch = match.ObservableResult;
if (match.ImmediateResult == true)
foreach (var animation in Animations)
{
obsMatch = Observable.Return(true);
}
IObservable<bool> obsMatch = match.ObservableResult;
var sub = animation.Apply((Animatable)control, obsMatch);
subs.Add(sub);
if (match.ImmediateResult == true)
{
obsMatch = Observable.Return(true);
}
var sub = animation.Apply(animatable, null, obsMatch);
subs.Add(sub);
}
}
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>
public class TransformAnimator : Animator<double>
{
DoubleAnimator childKeyFrames;
DoubleAnimator childAnimator;
/// <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;
@ -36,15 +36,15 @@ namespace Avalonia.Animation
var renderTransformType = ctrl.RenderTransform.GetType();
if (childKeyFrames == null)
if (childAnimator == null)
{
InitializeChildKeyFrames();
InitializeChildAnimator();
}
// It's a transform object so let's target that.
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.
else if (renderTransformType == typeof(TransformGroup))
@ -53,7 +53,7 @@ namespace Avalonia.Animation
{
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;
}
void InitializeChildKeyFrames()
void InitializeChildAnimator()
{
childKeyFrames = new DoubleAnimator();
childAnimator = new DoubleAnimator();
foreach (AnimatorKeyFrame keyframe in this)
{
childKeyFrames.Add(keyframe);
childAnimator.Add(keyframe);
}
childKeyFrames.Property = Property;
childAnimator.Property = Property;
}
/// <inheritdocs/>

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

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup>
<ItemGroup>
<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.
/// </summary>
[TypeConverter(typeof(BrushConverter))]
public abstract class Brush : AvaloniaObject, IBrush
public abstract class Brush : AvaloniaObject, IMutableBrush
{
/// <summary>
/// Defines the <see cref="Opacity"/> property.
@ -18,6 +18,9 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<Brush, double>(nameof(Opacity), 1.0);
/// <inheritdoc/>
public event EventHandler Invalidated;
/// <summary>
/// Gets or sets the opacity of the brush.
/// </summary>
@ -50,5 +53,36 @@ namespace Avalonia.Media
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.
// 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.Specialized;
using System.ComponentModel;
using Avalonia.Collections;
using Avalonia.Metadata;
namespace Avalonia.Media
@ -20,35 +24,74 @@ namespace Avalonia.Media
/// <summary>
/// Defines the <see cref="GradientStops"/> property.
/// </summary>
public static readonly StyledProperty<IList<GradientStop>> GradientStopsProperty =
AvaloniaProperty.Register<GradientBrush, IList<GradientStop>>(nameof(GradientStops));
public static readonly StyledProperty<GradientStops> GradientStopsProperty =
AvaloniaProperty.Register<GradientBrush, GradientStops>(nameof(GradientStops));
private IDisposable _gradientStopsSubscription;
static GradientBrush()
{
GradientStopsProperty.Changed.Subscribe(GradientStopsChanged);
AffectsRender<LinearGradientBrush>(SpreadMethodProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="GradientBrush"/> class.
/// </summary>
public GradientBrush()
{
this.GradientStops = new List<GradientStop>();
this.GradientStops = new GradientStops();
}
/// <summary>
/// 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>
/// <inheritdoc/>
public GradientSpreadMethod SpreadMethod
{
get { return GetValue(SpreadMethodProperty); }
set { SetValue(SpreadMethodProperty, value); }
}
/// <summary>
/// Gets or sets the brush's gradient stops.
/// </summary>
/// <inheritdoc/>
[Content]
public IList<GradientStop> GradientStops
public GradientStops GradientStops
{
get { return GetValue(GradientStopsProperty); }
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
{
/// <summary>
/// GradientStop
/// Describes the location and color of a transition point in a gradient.
/// </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>
/// Initializes a new instance of the <see cref="GradientStop"/> class.
/// </summary>
@ -24,16 +36,18 @@ namespace Avalonia.Media
Offset = offset;
}
// TODO: Make these dependency properties.
/// <summary>
/// The offset
/// </summary>
public double Offset { get; set; }
/// <inheritdoc/>
public double Offset
{
get => GetValue(OffsetProperty);
set => SetValue(OffsetProperty, value);
}
/// <summary>
/// The color
/// </summary>
public Color Color { get; set; }
/// <inheritdoc/>
public Color Color
{
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>
/// Gets the brush's gradient stops.
/// </summary>
IList<GradientStop> GradientStops { get; }
IReadOnlyList<IGradientStop> GradientStops { get; }
/// <summary>
/// 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>
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>
/// Represents a mutable brush which can return an immutable clone of itself.
/// </summary>
public interface IMutableBrush : IBrush
public interface IMutableBrush : IBrush, IAffectsRender
{
/// <summary>
/// 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.
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
namespace Avalonia.Media
{
/// <summary>
/// Paints an area with an <see cref="IBitmap"/>.
/// </summary>
public class ImageBrush : TileBrush, IImageBrush, IMutableBrush
public class ImageBrush : TileBrush, IImageBrush
{
/// <summary>
/// Defines the <see cref="Visual"/> property.
@ -16,6 +17,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<IBitmap> SourceProperty =
AvaloniaProperty.Register<ImageBrush, IBitmap>(nameof(Source));
static ImageBrush()
{
AffectsRender<ImageBrush>(SourceProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
@ -42,9 +48,9 @@ namespace Avalonia.Media
}
/// <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.Linq;
namespace Avalonia.Media.Immutable
{
@ -15,7 +14,7 @@ namespace Avalonia.Media.Immutable
/// <param name="opacity">The opacity of the brush.</param>
/// <param name="spreadMethod">The spread method.</param>
protected ImmutableGradientBrush(
IList<GradientStop> gradientStops,
IReadOnlyList<ImmutableGradientStop> gradientStops,
double opacity,
GradientSpreadMethod spreadMethod)
{
@ -28,14 +27,14 @@ namespace Avalonia.Media.Immutable
/// Initializes a new instance of the <see cref="ImmutableGradientBrush"/> class.
/// </summary>
/// <param name="source">The brush from which this brush's properties should be copied.</param>
protected ImmutableGradientBrush(IGradientBrush source)
: this(source.GradientStops.ToList(), source.Opacity, source.SpreadMethod)
protected ImmutableGradientBrush(GradientBrush source)
: this(source.GradientStops.ToImmutable(), source.Opacity, source.SpreadMethod)
{
}
/// <inheritdoc/>
public IList<GradientStop> GradientStops { get; }
public IReadOnlyList<IGradientStop> GradientStops { get; }
/// <inheritdoc/>
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="endPoint">The end point for the gradient.</param>
public ImmutableLinearGradientBrush(
IList<GradientStop> gradientStops,
IReadOnlyList<ImmutableGradientStop> gradientStops,
double opacity = 1,
GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad,
RelativePoint? startPoint = null,
@ -31,7 +31,7 @@ namespace Avalonia.Media.Immutable
/// Initializes a new instance of the <see cref="ImmutableLinearGradientBrush"/> class.
/// </summary>
/// <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)
{
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.
/// </param>
public ImmutableRadialGradientBrush(
IList<GradientStop> gradientStops,
IReadOnlyList<ImmutableGradientStop> gradientStops,
double opacity = 1,
GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad,
RelativePoint? center = null,
@ -38,7 +38,7 @@ namespace Avalonia.Media.Immutable
/// Initializes a new instance of the <see cref="ImmutableRadialGradientBrush"/> class.
/// </summary>
/// <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)
{
Center = source.Center;

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

@ -1,12 +1,14 @@
// 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.Media.Immutable;
namespace Avalonia.Media
{
/// <summary>
/// A brush that draws with a linear gradient.
/// </summary>
public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush, IMutableBrush
public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush
{
/// <summary>
/// Defines the <see cref="StartPoint"/> property.
@ -24,6 +26,11 @@ namespace Avalonia.Media
nameof(EndPoint),
RelativePoint.BottomRight);
static LinearGradientBrush()
{
AffectsRender<LinearGradientBrush>(StartPointProperty, EndPointProperty);
}
/// <summary>
/// Gets or sets the start point for the gradient.
/// </summary>
@ -43,9 +50,9 @@ namespace Avalonia.Media
}
/// <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.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media.Immutable;
namespace Avalonia.Media
{
/// <summary>
/// Paints an area with a radial gradient.
/// </summary>
public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush, IMutableBrush
public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush
{
/// <summary>
/// Defines the <see cref="Center"/> property.
@ -63,9 +65,9 @@ namespace Avalonia.Media
}
/// <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.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media.Immutable;
namespace Avalonia.Media
{
/// <summary>
/// Fills an area with a solid color.
/// </summary>
public class SolidColorBrush : Brush, ISolidColorBrush, IMutableBrush
public class SolidColorBrush : Brush, ISolidColorBrush
{
/// <summary>
/// Defines the <see cref="Color"/> property.
@ -14,6 +16,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<SolidColorBrush, Color>(nameof(Color));
static SolidColorBrush()
{
AffectsRender<SolidColorBrush>(ColorProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="SolidColorBrush"/> class.
/// </summary>
@ -75,9 +82,9 @@ namespace Avalonia.Media
}
/// <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()
{
AffectsRender<TileBrush>(
AlignmentXProperty,
AlignmentYProperty,
DestinationRectProperty,
SourceRectProperty,
StretchProperty,
TileModeProperty);
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.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media.Immutable;
using Avalonia.VisualTree;
namespace Avalonia.Media
@ -8,7 +9,7 @@ namespace Avalonia.Media
/// <summary>
/// Paints an area with an <see cref="IVisual"/>.
/// </summary>
public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush
public class VisualBrush : TileBrush, IVisualBrush
{
/// <summary>
/// Defines the <see cref="Visual"/> property.
@ -16,6 +17,11 @@ namespace Avalonia.Media
public static readonly StyledProperty<IVisual> VisualProperty =
AvaloniaProperty.Register<VisualBrush, IVisual>(nameof(Visual));
static VisualBrush()
{
AffectsRender<VisualBrush>(VisualProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="VisualBrush"/> class.
/// </summary>
@ -42,9 +48,9 @@ namespace Avalonia.Media
}
/// <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.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Avalonia.Platform;
namespace Avalonia.Rendering
{
/// <summary>
/// Defines a default render loop that uses a standard timer.
/// Defines a default render timer that uses a standard timer.
/// </summary>
/// <remarks>
/// This class may be overridden by platform implementations to use a specialized timer
/// implementation.
/// </remarks>
public class DefaultRenderLoop : IRenderLoop
public class DefaultRenderTimer : IRenderTimer
{
private IRuntimePlatform _runtime;
private int _subscriberCount;
private EventHandler<EventArgs> _tick;
private Action<TimeSpan> _tick;
private IDisposable _subscription;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultRenderLoop"/> class.
/// Initializes a new instance of the <see cref="DefaultRenderTimer"/> class.
/// </summary>
/// <param name="framesPerSecond">
/// The number of frames per second at which the loop should run.
/// </param>
public DefaultRenderLoop(int framesPerSecond)
public DefaultRenderTimer(int framesPerSecond)
{
FramesPerSecond = framesPerSecond;
}
@ -37,7 +39,7 @@ namespace Avalonia.Rendering
public int FramesPerSecond { get; }
/// <inheritdoc/>
public event EventHandler<EventArgs> Tick
public event Action<TimeSpan> Tick
{
add
{
@ -76,14 +78,16 @@ namespace Avalonia.Rendering
/// This can be overridden by platform implementations to use a specialized timer
/// implementation.
/// </remarks>
protected virtual IDisposable StartCore(Action tick)
protected virtual IDisposable StartCore(Action<TimeSpan> tick)
{
if (_runtime == null)
{
_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>
@ -95,9 +99,9 @@ namespace Avalonia.Rendering
_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.Utilities;
using Avalonia.VisualTree;
using System.Threading.Tasks;
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
/// representation which is then rendered on a rendering thread.
/// </summary>
public class DeferredRenderer : RendererBase, IRenderer, IVisualBrushRenderer
public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer
{
private readonly IDispatcher _dispatcher;
private readonly IRenderLoop _renderLoop;
@ -31,7 +32,6 @@ namespace Avalonia.Rendering
private volatile IRef<Scene> _scene;
private DirtyVisuals _dirty;
private IRef<IRenderTargetBitmapImpl> _overlay;
private bool _updateQueued;
private object _rendering = new object();
private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
@ -149,7 +149,7 @@ namespace Avalonia.Rendering
{
if (!_running && _renderLoop != null)
{
_renderLoop.Tick += OnRenderLoopTick;
_renderLoop.Add(this);
_running = true;
}
}
@ -159,11 +159,23 @@ namespace Avalonia.Rendering
{
if (_running && _renderLoop != null)
{
_renderLoop.Tick -= OnRenderLoopTick;
_renderLoop.Remove(this);
_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/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
@ -381,67 +393,34 @@ namespace Avalonia.Rendering
private void UpdateScene()
{
Dispatcher.UIThread.VerifyAccess();
try
if (_root.IsVisible)
{
if (_root.IsVisible)
{
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();
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
var scene = sceneRef.Item;
_dirty.Clear();
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
}
else
if (_dirty == null)
{
var oldScene = Interlocked.Exchange(ref _scene, null);
oldScene?.Dispose();
_dirty = new DirtyVisuals();
_sceneBuilder.UpdateAll(scene);
}
}
finally
{
_updateQueued = false;
}
}
private void OnRenderLoopTick(object sender, EventArgs e)
{
if (Monitor.TryEnter(_rendering))
{
try
else if (_dirty.Count > 0)
{
if (!_updateQueued && (_dirty == null || _dirty.Count > 0))
foreach (var visual in _dirty)
{
_updateQueued = true;
_dispatcher.Post(UpdateScene, DispatcherPriority.Render);
}
using (var scene = _scene?.Clone())
{
Render(scene?.Item);
_sceneBuilder.Update(scene, visual);
}
}
catch { }
finally
{
Monitor.Exit(_rendering);
}
var oldScene = Interlocked.Exchange(ref _scene, sceneRef);
oldScene?.Dispose();
_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>
/// Defines the interface implemented by an application render loop.
/// 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 interface IRenderLoop
{
/// <summary>
/// Raised when the render loop ticks to signal a new frame should be drawn.
/// Adds an update task.
/// </summary>
/// <param name="i">The update task.</param>
/// <remarks>
/// This event can be raised on any thread; it is the responsibility of the subscriber to
/// switch execution to the right thread.
/// Registered update tasks will be polled on each tick of the render loop after the
/// animation timer has been pulsed.
/// </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)
{
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)
{

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.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : class, IVisual
where T : Visual
{
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)
@ -544,6 +557,8 @@ namespace Avalonia
OnVisualParentChanged(old, value);
}
private void AffectsRenderInvalidated(object sender, EventArgs e) => InvalidateVisual();
/// <summary>
/// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary>

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

@ -52,7 +52,8 @@ namespace Avalonia.Gtk3
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(Instance)
.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());
}

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

@ -35,7 +35,8 @@ namespace Avalonia.LinuxFramebuffer
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.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()

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

@ -21,6 +21,7 @@ namespace Avalonia.MonoMac
private static bool s_monoMacInitialized;
private static bool s_showInDock = true;
private static IRenderLoop s_renderLoop;
private static IRenderTimer s_renderTimer;
void DoInitialize()
{
@ -35,6 +36,7 @@ namespace Avalonia.MonoMac
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsImpl>()
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<IRenderLoop>().ToConstant(s_renderLoop)
.Bind<IRenderTimer>().ToConstant(s_renderTimer)
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
/*.Bind<IPlatformDragSource>().ToTransient<DragSource>()*/;
}
@ -83,7 +85,8 @@ namespace Avalonia.MonoMac
ThreadHelper.InitializeCocoaThreadingLocks();
App = NSApplication.SharedApplication;
UpdateActivationPolicy();
s_renderLoop = new RenderLoop(); //TODO: use CVDisplayLink
s_renderLoop = new RenderLoop();
s_renderTimer = new RenderTimer(60); //TODO: use CVDisplayLink
s_monoMacInitialized = true;
}
@ -133,4 +136,4 @@ namespace Avalonia
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);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void WaitOrTimerCallback(IntPtr lpParameter, bool timerOrWaitFired);
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
public enum Cursor
@ -848,11 +851,25 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow);
[DllImport("Winmm.dll")]
public static extern uint timeKillEvent(uint uTimerID);
[DllImport("kernel32.dll", SetLastError = true)]
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")]
public static extern uint timeSetEvent(uint uDelay, uint uResolution, TimeCallback lpTimeProc, UIntPtr dwUser, uint fuEvent);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent);
[DllImport("user32.dll")]
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.Runtime.InteropServices;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
@ -82,7 +83,8 @@ namespace Avalonia.Win32
.Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
.Bind<IPlatformSettings>().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<IWindowingPlatform>().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
{
class DisplayLinkRenderLoop : IRenderLoop
class DisplayLinkRenderTimer : IRenderTimer
{
public event EventHandler<EventArgs> Tick;
public event Action<TimeSpan> Tick;
private CADisplayLink _link;
public DisplayLinkRenderLoop()
public DisplayLinkRenderTimer()
{
_link = CADisplayLink.Create(OnFrame);
@ -20,7 +21,7 @@ namespace Avalonia.iOS
{
try
{
Tick?.Invoke(this, new EventArgs());
Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
}
catch (Exception)
{

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

@ -41,7 +41,8 @@ namespace Avalonia.iOS
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.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);
}
[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>
/// Returns an observable that returns a single value but does not complete.
/// </summary>
@ -595,4 +607,4 @@ namespace Avalonia.Base.UnitTests
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.
// 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;
namespace Avalonia.Controls.UnitTests
@ -42,5 +46,22 @@ namespace Avalonia.Controls.UnitTests
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 Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
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.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 Xunit;
using Avalonia.Rendering;
using Avalonia.Media;
namespace Avalonia.Controls.UnitTests.Presenters
{
@ -203,5 +204,22 @@ namespace Avalonia.Controls.UnitTests.Presenters
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