diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 8f1b39ae12..3f313ead1b 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all diff --git a/packages/Avalonia/Avalonia.targets b/packages/Avalonia/Avalonia.targets index 50306f2cdc..eacd287b59 100644 --- a/packages/Avalonia/Avalonia.targets +++ b/packages/Avalonia/Avalonia.targets @@ -1,3 +1,4 @@ + diff --git a/packages/Avalonia/AvaloniaPrivateApis.targets b/packages/Avalonia/AvaloniaPrivateApis.targets new file mode 100644 index 0000000000..5343559c07 --- /dev/null +++ b/packages/Avalonia/AvaloniaPrivateApis.targets @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + net6.0 + netstandard2.0 + + + + + + + + + diff --git a/samples/RenderDemo/Pages/CustomStringAnimator.cs b/samples/RenderDemo/Pages/CustomStringAnimator.cs index 398319726e..4aea131870 100644 --- a/samples/RenderDemo/Pages/CustomStringAnimator.cs +++ b/samples/RenderDemo/Pages/CustomStringAnimator.cs @@ -1,9 +1,10 @@ -using Avalonia.Animation; +using System; +using Avalonia.Animation; using Avalonia.Animation.Animators; namespace RenderDemo.Pages { - public class CustomStringAnimator : CustomAnimatorBase + public class CustomStringAnimator : InterpolatingAnimator { public override string Interpolate(double progress, string oldValue, string newValue) { diff --git a/src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs b/src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs new file mode 100644 index 0000000000..6aaafa1b96 --- /dev/null +++ b/src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Avalonia.Animation.Animators; +using Avalonia.Media; + +namespace Avalonia.Animation; + +partial class Animation +{ + /// + /// Sets the value of the Animator attached property for a setter. + /// + /// The animation setter. + /// The property animator value. + [Obsolete("CustomAnimatorBase will be removed before 11.0, use InterpolatingAnimator", true)] + public static void SetAnimator(IAnimationSetter setter, CustomAnimatorBase value) + { + s_animators[setter] = (value.WrapperType, value.CreateWrapper); + } + + /// + /// Sets the value of the Animator attached property for a setter. + /// + /// The animation setter. + /// The property animator value. + public static void SetAnimator(IAnimationSetter setter, ICustomAnimator value) + { + s_animators[setter] = (value.WrapperType, value.CreateWrapper); + } + + private readonly static List<(Func Condition, Type Animator, Func Factory)> + Animators = new() + { + (prop =>(typeof(double).IsAssignableFrom(prop.PropertyType) && typeof(Transform).IsAssignableFrom(prop.OwnerType)), + typeof(TransformAnimator), () => new TransformAnimator()), + (prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator()), + (prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator()), + (prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator()), + (prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator()), + (prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator()), + (prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator()), + (prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator()), + (prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator()), + (prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator()), + (prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator()), + (prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator()), + }; + + static Animation() + { + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + RegisterAnimator(); + } + + /// + /// Registers a that can handle + /// a value type that matches the specified condition. + /// + static void RegisterAnimator() + where TAnimator : Animator, new() + { + Animators.Insert(0, + (prop => typeof(T).IsAssignableFrom(prop.PropertyType), typeof(TAnimator), () => new TAnimator())); + } + + public static void RegisterCustomAnimator() where TAnimator : InterpolatingAnimator, new() + { + Animators.Insert(0, (prop => typeof(T).IsAssignableFrom(prop.PropertyType), + typeof(InterpolatingAnimator.AnimatorWrapper), () => new TAnimator().CreateWrapper())); + } + + private static (Type Type, Func Factory)? GetAnimatorType(AvaloniaProperty property) + { + foreach (var (condition, type, factory) in Animators) + { + if (condition(property)) + { + return (type, factory); + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index 4de89bfe97..f584cad951 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/src/Avalonia.Base/Animation/Animation.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Reactive; using System.Threading; using System.Threading.Tasks; - -using Avalonia.Animation.Animators; using Avalonia.Animation.Easings; using Avalonia.Data; using Avalonia.Metadata; @@ -16,7 +13,7 @@ namespace Avalonia.Animation /// /// Tracks the progress of an animation. /// - public sealed class Animation : AvaloniaObject, IAnimation + public sealed partial class Animation : AvaloniaObject, IAnimation { /// /// Defines the property. @@ -195,60 +192,6 @@ namespace Avalonia.Animation return null; } - /// - /// Sets the value of the Animator attached property for a setter. - /// - /// The animation setter. - /// The property animator value. - public static void SetAnimator(IAnimationSetter setter, CustomAnimatorBase value) - { - s_animators[setter] = (value.WrapperType, value.CreateWrapper); - } - - private readonly static List<(Func Condition, Type Animator, Func Factory)> Animators = new() - { - ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator() ), - ( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator() ), - ( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator() ), - ( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator() ), - ( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator() ), - ( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator() ), - ( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator() ), - ( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator() ), - ( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator() ), - ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator() ), - ( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator() ), - }; - - /// - /// Registers a that can handle - /// a value type that matches the specified condition. - /// - /// - /// The condition to which the - /// is to be activated and used. - /// - /// - /// The type of the animator to instantiate. - /// - internal static void RegisterAnimator(Func condition) - where TAnimator : IAnimator, new() - { - Animators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator())); - } - - private static (Type Type, Func Factory)? GetAnimatorType(AvaloniaProperty property) - { - foreach (var (condition, type, factory) in Animators) - { - if (condition(property)) - { - return (type, factory); - } - } - return null; - } - private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { var handlerList = new Dictionary<(Type type, AvaloniaProperty Property), Func>(); diff --git a/src/Avalonia.Base/Animation/ICustomAnimator.cs b/src/Avalonia.Base/Animation/ICustomAnimator.cs index 88c9974ca6..119a6115da 100644 --- a/src/Avalonia.Base/Animation/ICustomAnimator.cs +++ b/src/Avalonia.Base/Animation/ICustomAnimator.cs @@ -1,14 +1,15 @@ using System; using Avalonia.Animation.Animators; - namespace Avalonia.Animation; +[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator", true)] public abstract class CustomAnimatorBase { internal abstract IAnimator CreateWrapper(); internal abstract Type WrapperType { get; } } +[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator", true)] public abstract class CustomAnimatorBase : CustomAnimatorBase { public abstract T Interpolate(double progress, T oldValue, T newValue); @@ -25,6 +26,33 @@ public abstract class CustomAnimatorBase : CustomAnimatorBase _parent = parent; } + public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue); + } +} + +public interface ICustomAnimator +{ + internal IAnimator CreateWrapper(); + internal Type WrapperType { get; } +} + +public abstract class InterpolatingAnimator : ICustomAnimator +{ + public abstract T Interpolate(double progress, T oldValue, T newValue); + + Type ICustomAnimator.WrapperType => typeof(AnimatorWrapper); + IAnimator ICustomAnimator.CreateWrapper() => new AnimatorWrapper(this); + internal IAnimator CreateWrapper() => new AnimatorWrapper(this); + + internal class AnimatorWrapper : Animator + { + private readonly InterpolatingAnimator _parent; + + public AnimatorWrapper(InterpolatingAnimator parent) + { + _parent = parent; + } + public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Animation/InterpolatingTransitionBase.cs b/src/Avalonia.Base/Animation/InterpolatingTransitionBase.cs new file mode 100644 index 0000000000..44e3129b76 --- /dev/null +++ b/src/Avalonia.Base/Animation/InterpolatingTransitionBase.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Animation.Animators; + +namespace Avalonia.Animation; + +/// +/// The base class for user-defined transition that are doing simple value interpolation +/// +public abstract class InterpolatingTransitionBase : Transition +{ + class Animator : Animator + { + private readonly InterpolatingTransitionBase _parent; + + public Animator(InterpolatingTransitionBase parent) + { + _parent = parent; + } + + public override T Interpolate(double progress, T oldValue, T newValue) => + _parent.Interpolate(progress, oldValue, newValue); + } + + protected abstract T Interpolate(double progress, T from, T to); + + internal override IObservable DoTransition(IObservable progress, T oldValue, T newValue) => + new AnimatorTransitionObservable(new Animator(this), progress, Easing, oldValue, newValue); +} \ No newline at end of file diff --git a/src/Avalonia.Base/CornerRadius.cs b/src/Avalonia.Base/CornerRadius.cs index 82791999d7..44b45c2a73 100644 --- a/src/Avalonia.Base/CornerRadius.cs +++ b/src/Avalonia.Base/CornerRadius.cs @@ -15,13 +15,6 @@ namespace Avalonia #endif readonly struct CornerRadius : IEquatable { - static CornerRadius() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(CornerRadius).IsAssignableFrom(prop.PropertyType)); -#endif - } - public CornerRadius(double uniformRadius) { TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius; diff --git a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizer.cs new file mode 100644 index 0000000000..2cb97b4707 --- /dev/null +++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizer.cs @@ -0,0 +1,37 @@ +namespace Avalonia.Input.GestureRecognizers +{ + public abstract class GestureRecognizer : StyledElement + { + protected internal IInputElement? Target { get; internal set; } + + protected abstract void PointerPressed(PointerPressedEventArgs e); + protected abstract void PointerReleased(PointerReleasedEventArgs e); + protected abstract void PointerMoved(PointerEventArgs e); + protected abstract void PointerCaptureLost(IPointer pointer); + + internal void PointerPressedInternal(PointerPressedEventArgs e) + { + PointerPressed(e); + } + + internal void PointerReleasedInternal(PointerReleasedEventArgs e) + { + PointerReleased(e); + } + + internal void PointerMovedInternal(PointerEventArgs e) + { + PointerMoved(e); + } + + internal void PointerCaptureLostInternal(IPointer pointer) + { + PointerCaptureLost(pointer); + } + + protected void Capture(IPointer pointer) + { + (pointer as Pointer)?.CaptureGestureRecognizer(this); + } + } +} diff --git a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs index 3b9b2d0de6..05dce8214b 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -6,29 +6,26 @@ using Avalonia.Reactive; namespace Avalonia.Input.GestureRecognizers { - public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher + public class GestureRecognizerCollection : IReadOnlyCollection { private readonly IInputElement _inputElement; - private List? _recognizers; - private Dictionary? _pointerGrabs; - + private List? _recognizers; public GestureRecognizerCollection(IInputElement inputElement) { _inputElement = inputElement; } - public void Add(IGestureRecognizer recognizer) + public void Add(GestureRecognizer recognizer) { if (_recognizers == null) { // We initialize the collection when the first recognizer is added - _recognizers = new List(); - _pointerGrabs = new Dictionary(); + _recognizers = new List(); } _recognizers.Add(recognizer); - recognizer.Initialize(_inputElement, this); + recognizer.Target = _inputElement; // Hacks to make bindings work @@ -41,25 +38,22 @@ namespace Avalonia.Input.GestureRecognizers } } - static readonly List s_Empty = new List(); + static readonly List s_Empty = new List(); - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() => _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public int Count => _recognizers?.Count ?? 0; - internal bool HandlePointerPressed(PointerPressedEventArgs e) { if (_recognizers == null) return false; foreach (var r in _recognizers) { - if (e.Handled) - break; - r.PointerPressed(e); + r.PointerPressedInternal(e); } return e.Handled; @@ -69,17 +63,15 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) + var pointer = e.Pointer as Pointer; + + foreach (var r in _recognizers) { - capture.PointerReleased(e); + if (pointer?.CapturedGestureRecognizer != null) + break; + + r.PointerReleasedInternal(e); } - else - foreach (var r in _recognizers) - { - if (e.Handled) - break; - r.PointerReleased(e); - } return e.Handled; } @@ -87,41 +79,16 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) - { - capture.PointerMoved(e); - } - else - foreach (var r in _recognizers) - { - if (e.Handled) - break; - r.PointerMoved(e); - } - return e.Handled; - } + var pointer = e.Pointer as Pointer; - internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e) - { - if (_recognizers == null) - return; - _pointerGrabs!.Remove(e.Pointer); foreach (var r in _recognizers) { - r.PointerCaptureLost(e.Pointer); - } - } + if (pointer?.CapturedGestureRecognizer != null) + break; - void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) - { - pointer.Capture(_inputElement); - _pointerGrabs![pointer] = recognizer; - foreach (var r in _recognizers!) - { - if (r != recognizer) - r.PointerCaptureLost(pointer); + r.PointerMovedInternal(e); } + return e.Handled; } - } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/IGestureRecognizer.cs deleted file mode 100644 index c1d9ae5304..0000000000 --- a/src/Avalonia.Base/Input/GestureRecognizers/IGestureRecognizer.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Avalonia.Input.GestureRecognizers -{ - public interface IGestureRecognizer - { - void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions); - void PointerPressed(PointerPressedEventArgs e); - void PointerReleased(PointerReleasedEventArgs e); - void PointerMoved(PointerEventArgs e); - void PointerCaptureLost(IPointer pointer); - } - - public interface IGestureRecognizerActionsDispatcher - { - void Capture(IPointer pointer, IGestureRecognizer recognizer); - } - - public enum GestureRecognizerResult - { - None, - Capture, - ReleaseCapture - } -} diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs index 3b83d0cb87..b02c82a066 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs @@ -2,10 +2,8 @@ namespace Avalonia.Input { - public class PinchGestureRecognizer : StyledElement, IGestureRecognizer + public class PinchGestureRecognizer : GestureRecognizer { - private IInputElement? _target; - private IGestureRecognizerActionsDispatcher? _actions; private float _initialDistance; private IPointer? _firstContact; private Point _firstPoint; @@ -13,12 +11,6 @@ namespace Avalonia.Input private Point _secondPoint; private Point _origin; - public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) - { - _target = target; - _actions = actions; - } - private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { PointerPressed(e); @@ -29,14 +21,14 @@ namespace Avalonia.Input PointerReleased(e); } - public void PointerCaptureLost(IPointer pointer) + protected override void PointerCaptureLost(IPointer pointer) { RemoveContact(pointer); } - public void PointerMoved(PointerEventArgs e) + protected override void PointerMoved(PointerEventArgs e) { - if (_target != null && _target is Visual visual) + if (Target != null && Target is Visual visual) { if(_firstContact == e.Pointer) { @@ -58,16 +50,16 @@ namespace Avalonia.Input var scale = distance / _initialDistance; var pinchEventArgs = new PinchEventArgs(scale, _origin); - _target?.RaiseEvent(pinchEventArgs); + Target?.RaiseEvent(pinchEventArgs); e.Handled = pinchEventArgs.Handled; } } } - public void PointerPressed(PointerPressedEventArgs e) + protected override void PointerPressed(PointerPressedEventArgs e) { - if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (Target != null && Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { if (_firstContact == null) { @@ -92,13 +84,13 @@ namespace Avalonia.Input _origin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f); - _actions!.Capture(_firstContact, this); - _actions!.Capture(_secondContact, this); + Capture(_firstContact); + Capture(_secondContact); } } } - public void PointerReleased(PointerReleasedEventArgs e) + protected override void PointerReleased(PointerReleasedEventArgs e) { RemoveContact(e.Pointer); } @@ -118,7 +110,7 @@ namespace Avalonia.Input _secondContact = null; } - _target?.RaiseEvent(new PinchEndedEventArgs()); + Target?.RaiseEvent(new PinchEndedEventArgs()); } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index 6784677520..57faf5bfd8 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -1,15 +1,11 @@ using System; -using System.Diagnostics; using Avalonia.Input.GestureRecognizers; namespace Avalonia.Input { - public class PullGestureRecognizer : StyledElement, IGestureRecognizer + public class PullGestureRecognizer : GestureRecognizer { internal static int MinPullDetectionSize = 50; - - private IInputElement? _target; - private IGestureRecognizerActionsDispatcher? _actions; private Point _initialPosition; private int _gestureId; private IPointer? _tracking; @@ -34,13 +30,7 @@ namespace Avalonia.Input public PullGestureRecognizer() { } - public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) - { - _target = target; - _actions = actions; - } - - public void PointerCaptureLost(IPointer pointer) + protected override void PointerCaptureLost(IPointer pointer) { if (_tracking == pointer) { @@ -48,12 +38,13 @@ namespace Avalonia.Input } } - public void PointerMoved(PointerEventArgs e) + protected override void PointerMoved(PointerEventArgs e) { - if (_tracking == e.Pointer && _target is Visual visual) + if (_tracking == e.Pointer && Target is Visual visual) { var currentPosition = e.GetPosition(visual); - _actions!.Capture(e.Pointer, this); + Capture(e.Pointer); + e.PreventGestureRecognition(); Vector delta = default; switch (PullDirection) @@ -86,15 +77,15 @@ namespace Avalonia.Input _pullInProgress = true; var pullEventArgs = new PullGestureEventArgs(_gestureId, delta, PullDirection); - _target?.RaiseEvent(pullEventArgs); + Target?.RaiseEvent(pullEventArgs); e.Handled = pullEventArgs.Handled; } } - public void PointerPressed(PointerPressedEventArgs e) + protected override void PointerPressed(PointerPressedEventArgs e) { - if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (Target != null && Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { var position = e.GetPosition(visual); @@ -127,7 +118,7 @@ namespace Avalonia.Input } } - public void PointerReleased(PointerReleasedEventArgs e) + protected override void PointerReleased(PointerReleasedEventArgs e) { if (_tracking == e.Pointer && _pullInProgress) { @@ -141,7 +132,7 @@ namespace Avalonia.Input _initialPosition = default; _pullInProgress = false; - _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); + Target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); } } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index b510d44e63..beecfcc3ce 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -4,7 +4,7 @@ using Avalonia.Threading; namespace Avalonia.Input.GestureRecognizers { - public class ScrollGestureRecognizer : AvaloniaObject, IGestureRecognizer + public class ScrollGestureRecognizer : GestureRecognizer { // Pixels per second speed that is considered to be the stop of inertial scroll internal const double InertialScrollSpeedEnd = 5; @@ -18,8 +18,6 @@ namespace Avalonia.Input.GestureRecognizers private bool _scrolling; private Point _trackedRootPoint; private IPointer? _tracking; - private IInputElement? _target; - private IGestureRecognizerActionsDispatcher? _actions; private int _gestureId; private Point _pointerPressedPoint; private VelocityTracker? _velocityTracker; @@ -91,15 +89,9 @@ namespace Avalonia.Input.GestureRecognizers { get => _scrollStartDistance; set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value); - } - - public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) - { - _target = target; - _actions = actions; } - - public void PointerPressed(PointerPressedEventArgs e) + + protected override void PointerPressed(PointerPressedEventArgs e) { if (e.Pointer.IsPrimary && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) @@ -107,15 +99,15 @@ namespace Avalonia.Input.GestureRecognizers EndGesture(); _tracking = e.Pointer; _gestureId = ScrollGestureEventArgs.GetNextFreeId(); - _trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target); + _trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)Target); } } - - public void PointerMoved(PointerEventArgs e) + + protected override void PointerMoved(PointerEventArgs e) { if (e.Pointer == _tracking) { - var rootPoint = e.GetPosition((Visual?)_target); + var rootPoint = e.GetPosition((Visual?)Target); if (!_scrolling) { if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance) @@ -131,7 +123,9 @@ namespace Avalonia.Input.GestureRecognizers _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance), _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance)); - _actions!.Capture(e.Pointer, this); + Capture(e.Pointer); + + e.PreventGestureRecognition(); } } @@ -143,13 +137,13 @@ namespace Avalonia.Input.GestureRecognizers _lastMoveTimestamp = e.Timestamp; _trackedRootPoint = rootPoint; - _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); + Target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); e.Handled = true; } } } - public void PointerCaptureLost(IPointer pointer) + protected override void PointerCaptureLost(IPointer pointer) { if (pointer == _tracking) EndGesture(); } @@ -161,7 +155,7 @@ namespace Avalonia.Input.GestureRecognizers { _inertia = default; _scrolling = false; - _target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); + Target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); _gestureId = 0; _lastMoveTimestamp = null; } @@ -169,7 +163,7 @@ namespace Avalonia.Input.GestureRecognizers } - public void PointerReleased(PointerReleasedEventArgs e) + protected override void PointerReleased(PointerReleasedEventArgs e) { if (e.Pointer == _tracking && _scrolling) { @@ -188,7 +182,7 @@ namespace Avalonia.Input.GestureRecognizers var savedGestureId = _gestureId; var st = Stopwatch.StartNew(); var lastTime = TimeSpan.Zero; - _target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia)); + Target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia)); DispatcherTimer.Run(() => { // Another gesture has started, finish the current one @@ -203,7 +197,7 @@ namespace Avalonia.Input.GestureRecognizers var speed = _inertia * Math.Pow(InertialResistance, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); - _target!.RaiseEvent(scrollGestureEventArgs); + Target!.RaiseEvent(scrollGestureEventArgs); if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture) { diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 167f61eead..e9d23632b1 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -182,7 +182,7 @@ namespace Avalonia.Input s_lastPressPoint = e.GetPosition((Visual)ev.Source); s_holdCancellationToken = new CancellationTokenSource(); var token = s_holdCancellationToken.Token; - var settings = AvaloniaLocator.Current.GetService(); + var settings = ((IInputRoot?)visual.GetVisualRoot())?.PlatformSettings; if (settings != null) { @@ -221,7 +221,7 @@ namespace Avalonia.Input e.Source is Interactive i) { var point = e.GetCurrentPoint((Visual)target); - var settings = AvaloniaLocator.Current.GetService(); + var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings; var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4); var tapRect = new Rect(s_lastPressPoint, new Size()) .Inflate(new Thickness(tapSize.Width, tapSize.Height)); @@ -260,10 +260,10 @@ namespace Avalonia.Input var e = (PointerEventArgs)ev; if (s_lastPress.TryGetTarget(out var target)) { - if (e.Pointer == s_lastPointer) + if (e.Pointer == s_lastPointer && ev.Source is Interactive i) { var point = e.GetCurrentPoint((Visual)target); - var settings = AvaloniaLocator.Current.GetService(); + var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings; var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4); var tapRect = new Rect(s_lastPressPoint, new Size()) .Inflate(new Thickness(tapSize.Width, tapSize.Height)); @@ -273,7 +273,7 @@ namespace Avalonia.Input return; } - if (s_isHolding && ev.Source is Interactive i) + if (s_isHolding) { i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer!.Type)); } diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs index d0921eea2d..2be5847220 100644 --- a/src/Avalonia.Base/Input/IInputRoot.cs +++ b/src/Avalonia.Base/Input/IInputRoot.cs @@ -1,4 +1,5 @@ using Avalonia.Metadata; +using Avalonia.Platform; namespace Avalonia.Input { @@ -17,10 +18,18 @@ namespace Avalonia.Input /// Gets focus manager of the root. /// /// - /// Focus manager can be null only if application wasn't initialized yet. + /// Focus manager can be null only if window wasn't initialized yet. /// IFocusManager? FocusManager { get; } + /// + /// Represents a contract for accessing top-level platform-specific settings. + /// + /// + /// PlatformSettings can be null only if window wasn't initialized yet. + /// + IPlatformSettings? PlatformSettings { get; } + /// /// Gets or sets the input element that the pointer is currently over. /// diff --git a/src/Avalonia.Base/Input/IKeyboardDevice.cs b/src/Avalonia.Base/Input/IKeyboardDevice.cs index 172b58068c..dfab1cc83d 100644 --- a/src/Avalonia.Base/Input/IKeyboardDevice.cs +++ b/src/Avalonia.Base/Input/IKeyboardDevice.cs @@ -43,7 +43,7 @@ namespace Avalonia.Input PenBarrelButton = 2048 } - [NotClientImplementable] + [PrivateApi] public interface IKeyboardDevice : IInputDevice { } diff --git a/src/Avalonia.Base/Input/IMouseDevice.cs b/src/Avalonia.Base/Input/IMouseDevice.cs index 00c436bf21..4fd1ccad22 100644 --- a/src/Avalonia.Base/Input/IMouseDevice.cs +++ b/src/Avalonia.Base/Input/IMouseDevice.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - [NotClientImplementable] + [PrivateApi] public interface IMouseDevice : IPointerDevice { } diff --git a/src/Avalonia.Base/Input/IPenDevice.cs b/src/Avalonia.Base/Input/IPenDevice.cs index 1cc0fcf76d..a6cb669f51 100644 --- a/src/Avalonia.Base/Input/IPenDevice.cs +++ b/src/Avalonia.Base/Input/IPenDevice.cs @@ -1,8 +1,11 @@ -namespace Avalonia.Input +using Avalonia.Metadata; + +namespace Avalonia.Input { /// /// Represents a pen/stylus device. /// + [PrivateApi] public interface IPenDevice : IPointerDevice { diff --git a/src/Avalonia.Base/Input/IPointer.cs b/src/Avalonia.Base/Input/IPointer.cs index 52605bb6ae..050adbabaa 100644 --- a/src/Avalonia.Base/Input/IPointer.cs +++ b/src/Avalonia.Base/Input/IPointer.cs @@ -1,3 +1,4 @@ +using Avalonia.Input.GestureRecognizers; using Avalonia.Metadata; namespace Avalonia.Input diff --git a/src/Avalonia.Base/Input/IPointerDevice.cs b/src/Avalonia.Base/Input/IPointerDevice.cs index e0aebda9c5..afcea9e4cb 100644 --- a/src/Avalonia.Base/Input/IPointerDevice.cs +++ b/src/Avalonia.Base/Input/IPointerDevice.cs @@ -3,7 +3,7 @@ using Avalonia.Metadata; namespace Avalonia.Input { - [NotClientImplementable] + [PrivateApi] public interface IPointerDevice : IInputDevice { /// diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 68131e5bf7..46f543d25b 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -225,6 +225,11 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); + + // Gesture only handlers + PointerMovedEvent.AddClassHandler((x, e) => x.OnGesturePointerMoved(e), handledEventsToo: true); + PointerPressedEvent.AddClassHandler((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true); + PointerReleasedEvent.AddClassHandler((x, e) => x.OnGesturePointerReleased(e), handledEventsToo: true); } public InputElement() @@ -583,10 +588,6 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerMoved(PointerEventArgs e) { - if (_gestureRecognizers?.HandlePointerMoved(e) == true) - { - e.Handled = true; - } } /// @@ -595,10 +596,6 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerPressed(PointerPressedEventArgs e) { - if (_gestureRecognizers?.HandlePointerPressed(e) == true) - { - e.Handled = true; - } } /// @@ -607,10 +604,33 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerReleased(PointerReleasedEventArgs e) { - if (_gestureRecognizers?.HandlePointerReleased(e) == true) - { - e.Handled = true; - } + } + + private void OnGesturePointerReleased(PointerReleasedEventArgs e) + { + if (!e.IsGestureRecognitionSkipped) + if (_gestureRecognizers?.HandlePointerReleased(e) == true) + { + e.Handled = true; + } + } + + private void OnGesturePointerPressed(PointerPressedEventArgs e) + { + if (!e.IsGestureRecognitionSkipped) + if (_gestureRecognizers?.HandlePointerPressed(e) == true) + { + e.Handled = true; + } + } + + private void OnGesturePointerMoved(PointerEventArgs e) + { + if (!e.IsGestureRecognitionSkipped) + if (_gestureRecognizers?.HandlePointerMoved(e) == true) + { + e.Handled = true; + } } /// @@ -619,7 +639,7 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { - _gestureRecognizers?.HandlePointerCaptureLost(e); + } /// diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs index 9fa097c4b1..4de0c2d233 100644 --- a/src/Avalonia.Base/Input/KeyEventArgs.cs +++ b/src/Avalonia.Base/Input/KeyEventArgs.cs @@ -5,8 +5,6 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - public IKeyboardDevice? Device { get; init; } - public Key Key { get; init; } public KeyModifiers KeyModifiers { get; init; } diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs index a81bc6b2e0..c5e3ef5bf0 100644 --- a/src/Avalonia.Base/Input/KeyboardDevice.cs +++ b/src/Avalonia.Base/Input/KeyboardDevice.cs @@ -192,7 +192,6 @@ namespace Avalonia.Input KeyEventArgs ev = new KeyEventArgs { RoutedEvent = routedEvent, - Device = this, Key = keyInput.Key, KeyModifiers = keyInput.Modifiers.ToKeyModifiers(), Source = element, @@ -241,7 +240,6 @@ namespace Avalonia.Input { var ev = new TextInputEventArgs() { - Device = this, Text = text.Text, Source = element, RoutedEvent = InputElement.TextInputEvent diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index f3e77433a9..db333dbd8b 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -2,9 +2,12 @@ using System; using System.Collections.Generic; using Avalonia.Reactive; using Avalonia.Input.Raw; +using Avalonia.Interactivity; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.VisualTree; +using Avalonia.Input.GestureRecognizers; #pragma warning disable CS0618 namespace Avalonia.Input @@ -126,9 +129,10 @@ namespace Avalonia.Input if (source != null) { _pointer.Capture(source); - if (source != null) + + var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings; + if (settings is not null) { - var settings = AvaloniaLocator.Current.GetRequiredService(); var doubleClickTime = settings.GetDoubleTapTime(PointerType.Mouse).TotalMilliseconds; var doubleClickSize = settings.GetDoubleTapSize(PointerType.Mouse); @@ -141,11 +145,12 @@ namespace Avalonia.Input _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, (Visual)root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; } + + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, _pointer, (Visual)root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; } return false; @@ -158,17 +163,21 @@ namespace Avalonia.Input device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var source = _pointer.Captured ?? hitTest; + var source = _pointer.CapturedGestureRecognizer?.Target ?? _pointer.Captured ?? hitTest; if (source is object) { var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)root, p, timestamp, properties, inputModifiers, intermediatePoints); - source.RaiseEvent(e); + if (_pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) + gestureRecognizer.PointerMovedInternal(e); + else + source.RaiseEvent(e); return e.Handled; } + return false; } @@ -178,15 +187,19 @@ namespace Avalonia.Input device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var source = _pointer.Captured ?? hitTest; + var source = _pointer.CapturedGestureRecognizer?.Target ?? _pointer.Captured ?? hitTest; if (source is not null) { var e = new PointerReleasedEventArgs(source, _pointer, (Visual)root, p, timestamp, props, inputModifiers, _lastMouseDownButton); - source?.RaiseEvent(e); + if (_pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) + gestureRecognizer.PointerReleasedInternal(e); + else + source?.RaiseEvent(e); _pointer.Capture(null); + _pointer.CaptureGestureRecognizer(null); _lastMouseDownButton = default; return e.Handled; } diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 832f32fb03..09bf18d3fd 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -2,9 +2,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Avalonia.Input.GestureRecognizers; using Avalonia.Input.Raw; +using Avalonia.Interactivity; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.VisualTree; + #pragma warning disable CS0618 namespace Avalonia.Input @@ -80,19 +84,23 @@ namespace Avalonia.Input if (source != null) { pointer.Capture(source); - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.GetDoubleTapTime(PointerType.Pen).TotalMilliseconds ?? 500; - var doubleClickSize = settings?.GetDoubleTapSize(PointerType.Pen) ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings; + if (settings is not null) { - _clickCount = 0; + var doubleClickTime = settings.GetDoubleTapTime(PointerType.Pen).TotalMilliseconds; + var doubleClickSize = settings.GetDoubleTapSize(PointerType.Pen); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); } - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); var e = new PointerPressedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); @@ -107,14 +115,17 @@ namespace Avalonia.Input KeyModifiers inputModifiers, IInputElement? hitTest, Lazy?>? intermediatePoints) { - var source = pointer.Captured ?? hitTest; + var source = pointer.CapturedGestureRecognizer?.Target ?? pointer.Captured ?? hitTest; if (source is not null) { var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, (Visual)root, p, timestamp, properties, inputModifiers, intermediatePoints); - source.RaiseEvent(e); + if (pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) + gestureRecognizer.PointerMovedInternal(e); + else + source.RaiseEvent(e); return e.Handled; } @@ -125,15 +136,19 @@ namespace Avalonia.Input IInputElement root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers, IInputElement? hitTest) { - var source = pointer.Captured ?? hitTest; + var source = pointer.CapturedGestureRecognizer?.Target ?? pointer.Captured ?? hitTest; if (source is not null) { var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers, _lastMouseDownButton); - source.RaiseEvent(e); + if (pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) + gestureRecognizer.PointerReleasedInternal(e); + else + source.RaiseEvent(e); pointer.Capture(null); + pointer.CaptureGestureRecognizer(null); _lastMouseDownButton = default; return e.Handled; } diff --git a/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs index b87ca5eded..7ce79e2649 100644 --- a/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs +++ b/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs @@ -1,16 +1,20 @@ using System.Collections.Generic; - -#nullable enable +using Avalonia.Metadata; namespace Avalonia.Input.Platform { - public class PlatformHotkeyConfiguration + /// + /// The PlatformHotkeyConfiguration class represents a configuration for platform-specific hotkeys in an Avalonia application. + /// + public sealed class PlatformHotkeyConfiguration { + [PrivateApi] public PlatformHotkeyConfiguration() : this(KeyModifiers.Control) { } + [PrivateApi] public PlatformHotkeyConfiguration(KeyModifiers commandModifiers, KeyModifiers selectionModifiers = KeyModifiers.Shift, KeyModifiers wholeWordTextActionModifiers = KeyModifiers.Control) @@ -85,6 +89,22 @@ namespace Avalonia.Input.Platform { new KeyGesture(Key.Left, KeyModifiers.Alt) }; + PageLeft = new List + { + new KeyGesture(Key.PageUp, KeyModifiers.Shift) + }; + PageRight = new List + { + new KeyGesture(Key.PageDown, KeyModifiers.Shift) + }; + PageUp = new List + { + new KeyGesture(Key.PageUp) + }; + PageDown = new List + { + new KeyGesture(Key.PageDown) + }; } public KeyModifiers CommandModifiers { get; set; } @@ -106,5 +126,9 @@ namespace Avalonia.Input.Platform public List MoveCursorToTheEndOfDocumentWithSelection { get; set; } public List OpenContextMenu { get; set; } public List Back { get; set; } + public List PageUp { get; set; } + public List PageDown { get; set; } + public List PageRight { get; set; } + public List PageLeft { get; set; } } } diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 4713364f00..91358712a0 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Input.GestureRecognizers; using Avalonia.VisualTree; namespace Avalonia.Input @@ -52,6 +53,9 @@ namespace Avalonia.Input if (Captured is Visual v3) v3.DetachedFromVisualTree += OnCaptureDetached; + + if (Captured != null) + CaptureGestureRecognizer(null); } static IInputElement? GetNextCapture(Visual parent) @@ -69,6 +73,31 @@ namespace Avalonia.Input public PointerType Type { get; } public bool IsPrimary { get; } - public void Dispose() => Capture(null); + + /// + /// Gets the gesture recognizer that is currently capturing by the pointer, if any. + /// + internal GestureRecognizer? CapturedGestureRecognizer { get; private set; } + + public void Dispose() + { + Capture(null); + } + + /// + /// Captures pointer input to the specified gesture recognizer. + /// + /// The gesture recognizer. + /// + internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer) + { + if (CapturedGestureRecognizer != gestureRecognizer) + CapturedGestureRecognizer?.PointerCaptureLostInternal(this); + + if (gestureRecognizer != null) + Capture(null); + + CapturedGestureRecognizer = gestureRecognizer; + } } } diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 9c24e5c314..cd742cc4e4 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -58,6 +58,8 @@ namespace Avalonia.Input /// public ulong Timestamp { get; } + internal bool IsGestureRecognitionSkipped { get; private set; } + /// /// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated. /// @@ -121,6 +123,14 @@ namespace Avalonia.Input return points; } + /// + /// Prevents this event from being handled by other gesture recognizers in the route + /// + public void PreventGestureRecognition() + { + IsGestureRecognitionSkipped = true; + } + /// /// Returns the current pointer point properties /// diff --git a/src/Avalonia.Base/Input/Raw/IDragDropDevice.cs b/src/Avalonia.Base/Input/Raw/IDragDropDevice.cs index 3bcc9fadd3..5473b5a9e5 100644 --- a/src/Avalonia.Base/Input/Raw/IDragDropDevice.cs +++ b/src/Avalonia.Base/Input/Raw/IDragDropDevice.cs @@ -2,7 +2,7 @@ namespace Avalonia.Input.Raw { - [NotClientImplementable] + [PrivateApi] public interface IDragDropDevice : IInputDevice { } diff --git a/src/Avalonia.Base/Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs index cda0103749..f4acd694a0 100644 --- a/src/Avalonia.Base/Input/TextInputEventArgs.cs +++ b/src/Avalonia.Base/Input/TextInputEventArgs.cs @@ -4,8 +4,6 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - public IKeyboardDevice? Device { get; set; } - - public string? Text { get; set; } + public string? Text { get; init; } } } diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index 78b570da14..8868e966f0 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Avalonia.Input.Raw; +using Avalonia.Interactivity; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.VisualTree; + #pragma warning disable CS0618 namespace Avalonia.Input @@ -49,6 +52,7 @@ namespace Avalonia.Input } var target = pointer.Captured ?? args.Root; + var gestureTarget = pointer.CapturedGestureRecognizer?.Target; var updateKind = args.Type.ToUpdateKind(); var keyModifier = args.InputModifiers.ToKeyModifiers(); @@ -62,19 +66,23 @@ namespace Avalonia.Input } else { - var settings = AvaloniaLocator.Current.GetRequiredService(); - var doubleClickTime = settings.GetDoubleTapTime(PointerType.Touch).TotalMilliseconds; - var doubleClickSize = settings.GetDoubleTapSize(PointerType.Touch); - - if (!_lastClickRect.Contains(args.Position) - || ev.Timestamp - _lastClickTime > doubleClickTime) + var settings = ((IInputRoot?)(target as Interactive)?.GetVisualRoot())?.PlatformSettings; + if (settings is not null) { - _clickCount = 0; + var doubleClickTime = settings.GetDoubleTapTime(PointerType.Touch).TotalMilliseconds; + var doubleClickSize = settings.GetDoubleTapSize(PointerType.Touch); + + if (!_lastClickRect.Contains(args.Position) + || ev.Timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = ev.Timestamp; + _lastClickRect = new Rect(args.Position, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); } - ++_clickCount; - _lastClickTime = ev.Timestamp; - _lastClickRect = new Rect(args.Position, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); } target.RaiseEvent(new PointerPressedEventArgs(target, pointer, @@ -88,10 +96,19 @@ namespace Avalonia.Input _pointers.Remove(args.RawPointerId); using (pointer) { - target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, - (Visual)args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind), - keyModifier, MouseButton.Left)); + target = gestureTarget ?? target; + var e = new PointerReleasedEventArgs(target, pointer, + (Visual)args.Root, args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind), + keyModifier, MouseButton.Left); + if (gestureTarget != null) + { + pointer?.CapturedGestureRecognizer?.PointerReleasedInternal(e); + } + else + { + target.RaiseEvent(e); + } } } @@ -99,15 +116,28 @@ namespace Avalonia.Input { _pointers.Remove(args.RawPointerId); using (pointer) - pointer.Capture(null); + { + pointer?.Capture(null); + pointer?.CaptureGestureRecognizer(null); + } } if (args.Type == RawPointerEventType.TouchUpdate) { - target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, (Visual)args.Root, + target = gestureTarget ?? target; + var e = new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer!, (Visual)args.Root, args.Position, ev.Timestamp, new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind), - keyModifier, args.IntermediatePoints)); + keyModifier, args.IntermediatePoints); + + if (gestureTarget != null) + { + pointer?.CapturedGestureRecognizer?.PointerMovedInternal(e); + } + else + { + target.RaiseEvent(e); + } } } diff --git a/src/Avalonia.Base/Layout/ILayoutManager.cs b/src/Avalonia.Base/Layout/ILayoutManager.cs index 063c8e99d2..5035d3a48d 100644 --- a/src/Avalonia.Base/Layout/ILayoutManager.cs +++ b/src/Avalonia.Base/Layout/ILayoutManager.cs @@ -6,8 +6,8 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - [NotClientImplementable] - internal interface ILayoutManager : IDisposable + [PrivateApi] + public interface ILayoutManager : IDisposable { /// /// Raised when the layout manager completes a layout pass. diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 6d698ae5ce..c64c2714c3 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -2,9 +2,9 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using Avalonia.Logging; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.Utilities; @@ -16,7 +16,8 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - internal class LayoutManager : ILayoutManager, IDisposable + [PrivateApi] + public class LayoutManager : ILayoutManager, IDisposable { private const int MaxPasses = 10; private readonly Layoutable _owner; diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index 32b2f7a2fb..91529353ed 100644 --- a/src/Avalonia.Base/Media/BoxShadow.cs +++ b/src/Avalonia.Base/Media/BoxShadow.cs @@ -16,12 +16,6 @@ namespace Avalonia.Media public Color Color { get; set; } public bool IsInset { get; set; } - static BoxShadow() - { - Animation.Animation.RegisterAnimator(prop => - typeof(BoxShadow).IsAssignableFrom(prop.PropertyType)); - } - public bool Equals(in BoxShadow other) { return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color); diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index ca16452a96..385f73f703 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -10,12 +10,6 @@ namespace Avalonia.Media private readonly BoxShadow _first; private readonly BoxShadow[]? _list; public int Count { get; } - - static BoxShadows() - { - Animation.Animation.RegisterAnimator(prop => - typeof(BoxShadows).IsAssignableFrom(prop.PropertyType)); - } public BoxShadows(BoxShadow shadow) { diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index 986e3221e6..21be08b4af 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -34,11 +34,6 @@ namespace Avalonia.Media /// public static readonly StyledProperty TransformOriginProperty = AvaloniaProperty.Register(nameof(TransformOrigin)); - - static Brush() - { - Animation.Animation.RegisterAnimator(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType)); - } /// /// Gets or sets the opacity of the brush. diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 3ee151389a..17ee14e533 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -25,13 +25,6 @@ namespace Avalonia.Media { private const double byteToDouble = 1.0 / 255; - static Color() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(Color).IsAssignableFrom(prop.PropertyType)); -#endif - } - /// /// Gets the Alpha component of the color. /// diff --git a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs index 8353afa6ab..cdf6aa6b63 100644 --- a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs +++ b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs @@ -63,8 +63,6 @@ internal class EffectAnimator : Animator if(s_Registered) return; s_Registered = true; - Animation.RegisterAnimator(prop => - typeof(IEffect).IsAssignableFrom(prop.PropertyType)); } } diff --git a/src/Avalonia.Base/Media/MediaContext.Clock.cs b/src/Avalonia.Base/Media/MediaContext.Clock.cs index dc2a39a822..ea8ac13e06 100644 --- a/src/Avalonia.Base/Media/MediaContext.Clock.cs +++ b/src/Avalonia.Base/Media/MediaContext.Clock.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Avalonia.Animation; using Avalonia.Reactive; using Avalonia.Threading; +using Avalonia.Utilities; namespace Avalonia.Media; @@ -17,8 +18,12 @@ internal partial class MediaContext { private readonly MediaContext _parent; private List> _observers = new(); - public bool HasNewSubscriptions { get; set; } - public bool HasSubscriptions => _observers.Count > 0; + private List> _newObservers = new(); + private Queue> _queuedAnimationFrames = new(); + private Queue> _queuedAnimationFramesNext = new(); + private TimeSpan _currentAnimationTimestamp; + public bool HasNewSubscriptions => _newObservers.Count > 0; + public bool HasSubscriptions => _observers.Count > 0 || _queuedAnimationFrames.Count > 0; public MediaContextClock(MediaContext parent) { @@ -29,19 +34,41 @@ internal partial class MediaContext { _parent.ScheduleRender(false); Dispatcher.UIThread.VerifyAccess(); - HasNewSubscriptions = true; _observers.Add(observer); + _newObservers.Add(observer); return Disposable.Create(() => { Dispatcher.UIThread.VerifyAccess(); _observers.Remove(observer); }); } + + public void RequestAnimationFrame(Action action) + { + _parent.ScheduleRender(false); + _queuedAnimationFrames.Enqueue(action); + } public void Pulse(TimeSpan now) { + _newObservers.Clear(); + _currentAnimationTimestamp = now; + + // We are swapping the queues before enumeration + (_queuedAnimationFrames, _queuedAnimationFramesNext) = (_queuedAnimationFramesNext, _queuedAnimationFrames); + var animationFrames = _queuedAnimationFramesNext; + while (animationFrames.TryDequeue(out var callback)) + callback(now); + foreach (var observer in _observers.ToArray()) - observer.OnNext(now); + observer.OnNext(_currentAnimationTimestamp); + } + + public void PulseNewSubscriptions() + { + foreach (var observer in _newObservers.ToArray()) + observer.OnNext(_currentAnimationTimestamp); + _newObservers.Clear(); } public PlayState PlayState @@ -50,4 +77,6 @@ internal partial class MediaContext set => throw new InvalidOperationException(); } } + + public void RequestAnimationFrame(Action action) => _clock.RequestAnimationFrame(action); } \ No newline at end of file diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index 9bdd77960d..4ddc2ea9eb 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Rendering.Composition; @@ -78,7 +79,7 @@ partial class MediaContext // Nothing to do, and there are no pending commits return false; - foreach (var c in _requestedCommits) + foreach (var c in _requestedCommits.ToArray()) CommitCompositor(c); _requestedCommits.Clear(); diff --git a/src/Avalonia.Base/Media/MediaContext.cs b/src/Avalonia.Base/Media/MediaContext.cs index c261dd80b8..84a4a8c873 100644 --- a/src/Avalonia.Base/Media/MediaContext.cs +++ b/src/Avalonia.Base/Media/MediaContext.cs @@ -131,12 +131,11 @@ internal partial class MediaContext : ICompositorScheduler // We are doing several iterations when it happens for (var c = 0; c < 10; c++) { - _clock.HasNewSubscriptions = false; FireInvokeOnRenderCallbacks(); if (_clock.HasNewSubscriptions) { - _clock.Pulse(now); + _clock.PulseNewSubscriptions(); continue; } @@ -212,7 +211,12 @@ internal partial class MediaContext : ICompositorScheduler } while (count > 0); } - + + /// + /// Executes the callback in the next iteration of the current UI-thread + /// render loop / layout pass that. + /// + /// Code to execute. public void BeginInvokeOnRender(Action callback) { if (_invokeOnRenderCallbacks == null) @@ -224,4 +228,4 @@ internal partial class MediaContext : ICompositorScheduler if (!_isRendering) ScheduleRender(true); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Media/Transform.cs b/src/Avalonia.Base/Media/Transform.cs index 1ac81808a1..16ae137b39 100644 --- a/src/Avalonia.Base/Media/Transform.cs +++ b/src/Avalonia.Base/Media/Transform.cs @@ -15,12 +15,6 @@ namespace Avalonia.Media /// public abstract class Transform : Animatable, IMutableTransform, ICompositionRenderResource, ICompositorSerializable { - static Transform() - { - Animation.Animation.RegisterAnimator(prop => - typeof(ITransform).IsAssignableFrom(prop.OwnerType)); - } - internal Transform() { diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index 08fcdb50aa..a4d14b0084 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -1,12 +1,16 @@ using System; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Media; +using Avalonia.Metadata; +using Avalonia.VisualTree; namespace Avalonia.Platform { /// /// A default implementation of for platforms. /// + [PrivateApi] public class DefaultPlatformSettings : IPlatformSettings { public virtual Size GetTapSize(PointerType type) @@ -28,7 +32,10 @@ namespace Avalonia.Platform public virtual TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500); public virtual TimeSpan HoldWaitDuration => TimeSpan.FromMilliseconds(300); - + + public PlatformHotkeyConfiguration HotkeyConfiguration => + AvaloniaLocator.Current.GetRequiredService(); + public virtual PlatformColorValues GetColorValues() { return new PlatformColorValues diff --git a/src/Avalonia.Base/Platform/IPlatformSettings.cs b/src/Avalonia.Base/Platform/IPlatformSettings.cs index b167cf0edc..46980c6d51 100644 --- a/src/Avalonia.Base/Platform/IPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/IPlatformSettings.cs @@ -1,10 +1,15 @@ using System; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + /// + /// The interface represents a contract for accessing platform-specific settings and information. + /// Some of these settings might be changed by used globally in the OS in runtime. + /// + [NotClientImplementable] public interface IPlatformSettings { /// @@ -33,6 +38,11 @@ namespace Avalonia.Platform /// TimeSpan HoldWaitDuration { get; } + /// + /// Get a configuration for platform-specific hotkeys in an Avalonia application. + /// + PlatformHotkeyConfiguration HotkeyConfiguration { get; } + /// /// Gets current system color values including dark mode and accent colors. /// diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index f9f02ec339..0c455ab511 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -64,7 +64,8 @@ internal class BclStorageFile : IStorageBookmarkFile public Task OpenWriteAsync() { - return Task.FromResult(FileInfo.OpenWrite()); + var stream = new FileStream(FileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Write); + return Task.FromResult(stream); } public virtual Task SaveBookmarkAsync() diff --git a/src/Avalonia.Base/Point.cs b/src/Avalonia.Base/Point.cs index d11596d6be..331cce4a76 100644 --- a/src/Avalonia.Base/Point.cs +++ b/src/Avalonia.Base/Point.cs @@ -16,13 +16,6 @@ namespace Avalonia #endif readonly struct Point : IEquatable { - static Point() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(Point).IsAssignableFrom(prop.PropertyType)); -#endif - } - /// /// The X position. /// diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index d9218ab36e..433f46b66f 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -11,11 +11,6 @@ namespace Avalonia /// public readonly struct Rect : IEquatable { - static Rect() - { - Animation.Animation.RegisterAnimator(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType)); - } - /// /// The X position. /// diff --git a/src/Avalonia.Base/RelativePoint.cs b/src/Avalonia.Base/RelativePoint.cs index 71c6a5cc15..5f04f4d57f 100644 --- a/src/Avalonia.Base/RelativePoint.cs +++ b/src/Avalonia.Base/RelativePoint.cs @@ -54,13 +54,6 @@ namespace Avalonia private readonly RelativeUnit _unit; - static RelativePoint() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); -#endif - } - /// /// Initializes a new instance of the struct. /// diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index dde9dcd6fb..5838811e9e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -130,7 +130,7 @@ namespace Avalonia.Rendering.Composition Dispatcher.UIThread.VerifyAccess(); using var noPump = NonPumpingLockHelper.Use(); - _nextCommit ??= new(); + var commit = _nextCommit ??= new(); (_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead); while (_invokeBeforeCommitRead.Count > 0) @@ -188,7 +188,7 @@ namespace Avalonia.Rendering.Composition }, TaskContinuationOptions.ExecuteSynchronously); _nextCommit = null; - return _pendingBatch; + return commit; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 763ec3b5f6..7d6e9442d1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server; /// /// Server-side counterpart of /// -internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual +internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual, IServerRenderResourceObserver { #if DEBUG // This is needed for debugging purposes so we could see inspect the associated visual from debugger @@ -37,6 +37,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { _renderCommands?.Dispose(); _renderCommands = reader.ReadObject(); + _renderCommands?.AddObserver(this); } base.DeserializeChangesCore(reader, committedAt); } @@ -50,6 +51,8 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.RenderCore(canvas, currentTransformedClip); } + public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated(); + #if DEBUG public override string ToString() { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs index fad1995092..105580e6ad 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs @@ -13,8 +13,8 @@ internal interface IServerRenderResourceObserver internal interface IServerRenderResource : IServerRenderResourceObserver { - void AddObserver(IServerRenderResource observer); - void RemoveObserver(IServerRenderResource observer); + void AddObserver(IServerRenderResourceObserver observer); + void RemoveObserver(IServerRenderResourceObserver observer); void QueuedInvalidate(); } @@ -23,7 +23,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes private bool _pendingInvalidation; private bool _disposed; public bool IsDisposed => _disposed; - private RefCountingSmallDictionary _observers; + private RefCountingSmallDictionary _observers; public SimpleServerRenderResource(ServerCompositor compositor) : base(compositor) { @@ -97,7 +97,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes } - public void AddObserver(IServerRenderResource observer) + public void AddObserver(IServerRenderResourceObserver observer) { Debug.Assert(!_disposed); if(_disposed) @@ -105,7 +105,7 @@ internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderRes _observers.Add(observer); } - public void RemoveObserver(IServerRenderResource observer) + public void RemoveObserver(IServerRenderResourceObserver observer) { if (_disposed) return; diff --git a/src/Avalonia.Base/Rendering/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs index 371d34b1f4..820840afbc 100644 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Base/Rendering/IRenderRoot.cs @@ -16,9 +16,9 @@ namespace Avalonia.Rendering /// /// Gets the renderer for the window. /// - internal IRenderer Renderer { get; } + public IRenderer Renderer { get; } - internal IHitTester HitTester { get; } + public IHitTester HitTester { get; } /// /// The scaling factor to use in rendering. diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index 4cb79d803d..227ff53897 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -9,7 +9,8 @@ namespace Avalonia.Rendering /// /// Defines the interface for a renderer. /// - internal interface IRenderer : IDisposable + [PrivateApi] + public interface IRenderer : IDisposable { /// /// Gets a value indicating whether the renderer should draw specific diagnostics. @@ -73,7 +74,8 @@ namespace Avalonia.Rendering Compositor Compositor { get; } } - internal interface IHitTester + [PrivateApi] + public interface IHitTester { /// /// Hit tests a location to find the visuals at the specified point. diff --git a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs index fcccd9c752..552ecb14ff 100644 --- a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs +++ b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs @@ -1,11 +1,13 @@ using System; +using Avalonia.Metadata; namespace Avalonia.Rendering { /// /// Provides data for the event. /// - internal class SceneInvalidatedEventArgs : EventArgs + [PrivateApi] + public class SceneInvalidatedEventArgs : EventArgs { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 7781aec607..5ee4541571 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -15,13 +15,6 @@ namespace Avalonia #endif readonly struct Size : IEquatable { - static Size() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(Size).IsAssignableFrom(prop.PropertyType)); -#endif - } - /// /// A size representing infinity. /// diff --git a/src/Avalonia.Base/Thickness.cs b/src/Avalonia.Base/Thickness.cs index 9513d04782..9673898d09 100644 --- a/src/Avalonia.Base/Thickness.cs +++ b/src/Avalonia.Base/Thickness.cs @@ -15,13 +15,6 @@ namespace Avalonia #endif readonly struct Thickness : IEquatable { - static Thickness() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(Thickness).IsAssignableFrom(prop.PropertyType)); -#endif - } - /// /// The thickness on the left. /// diff --git a/src/Avalonia.Base/Utilities/ThrowHelper.cs b/src/Avalonia.Base/Utilities/ThrowHelper.cs new file mode 100644 index 0000000000..e97ab754e5 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ThrowHelper.cs @@ -0,0 +1,24 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities; + +/// +/// Helper method to help inlining methods that do a throw check. +/// Equivalent of .NET6+ ArgumentNullException.ThrowIfNull() for netstandard2.0+ +/// +internal class ThrowHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfNull([NotNull] object? argument, string paramName) + { + if (argument is null) + { + ThrowArgumentNullException(paramName); + } + } + + [DoesNotReturn] + private static void ThrowArgumentNullException(string paramName) => throw new ArgumentNullException(paramName); +} diff --git a/src/Avalonia.Base/Vector.cs b/src/Avalonia.Base/Vector.cs index 085f043627..166ae6b93b 100644 --- a/src/Avalonia.Base/Vector.cs +++ b/src/Avalonia.Base/Vector.cs @@ -17,13 +17,6 @@ namespace Avalonia #endif readonly struct Vector : IEquatable { - static Vector() - { -#if !BUILDTASK - Animation.Animation.RegisterAnimator(prop => typeof(Vector).IsAssignableFrom(prop.PropertyType)); -#endif - } - /// /// The X component. /// diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index b3cdd70fa0..e244323ed9 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Rendering; +using Avalonia.Utilities; namespace Avalonia.VisualTree { @@ -125,9 +128,9 @@ namespace Avalonia.VisualTree /// The visual's ancestors. public static IEnumerable GetVisualAncestors(this Visual visual) { - Visual? v = visual ?? throw new ArgumentNullException(nameof(visual)); + ThrowHelper.ThrowIfNull(visual, nameof(visual)); - v = v.VisualParent; + var v = visual.VisualParent; while (v != null) { @@ -194,7 +197,7 @@ namespace Avalonia.VisualTree /// The visual and its ancestors. public static IEnumerable GetSelfAndVisualAncestors(this Visual visual) { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); + ThrowHelper.ThrowIfNull(visual, nameof(visual)); yield return visual; @@ -275,7 +278,7 @@ namespace Avalonia.VisualTree /// The visual at the requested point. public static Visual? GetVisualAt(this Visual visual, Point p) { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); + ThrowHelper.ThrowIfNull(visual, nameof(visual)); return visual.GetVisualAt(p, x => x.IsVisible); } @@ -292,7 +295,7 @@ namespace Avalonia.VisualTree /// The visual at the requested point. public static Visual? GetVisualAt(this Visual visual, Point p, Func filter) { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); + ThrowHelper.ThrowIfNull(visual, nameof(visual)); var root = visual.GetVisualRoot(); @@ -321,7 +324,7 @@ namespace Avalonia.VisualTree this Visual visual, Point p) { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); + ThrowHelper.ThrowIfNull(visual, nameof(visual)); return visual.GetVisualsAt(p, x => x.IsVisible); } @@ -341,7 +344,7 @@ namespace Avalonia.VisualTree Point p, Func filter) { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); + ThrowHelper.ThrowIfNull(visual, nameof(visual)); var root = visual.GetVisualRoot(); @@ -435,7 +438,7 @@ namespace Avalonia.VisualTree /// public static IRenderRoot? GetVisualRoot(this Visual visual) { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); + ThrowHelper.ThrowIfNull(visual, nameof(visual)); return visual as IRenderRoot ?? visual.VisualRoot; } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index bfcd4750e3..7ade9f2437 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2660,25 +2660,25 @@ namespace Avalonia.Controls internal bool ProcessDownKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessDownKeyInternal(shift, ctrl); } internal bool ProcessEndKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessEndKey(shift, ctrl); } internal bool ProcessEnterKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessEnterKey(shift, ctrl); } internal bool ProcessHomeKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessHomeKey(shift, ctrl); } @@ -2718,25 +2718,25 @@ namespace Avalonia.Controls internal bool ProcessLeftKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessLeftKey(shift, ctrl); } internal bool ProcessNextKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessNextKey(shift, ctrl); } internal bool ProcessPriorKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessPriorKey(shift, ctrl); } internal bool ProcessRightKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessRightKey(shift, ctrl); } @@ -2854,7 +2854,7 @@ namespace Avalonia.Controls internal bool ProcessUpKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessUpKey(shift, ctrl); } @@ -3124,13 +3124,13 @@ namespace Avalonia.Controls //TODO: Ensure right button is checked for internal bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) { - KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); return UpdateStateOnMouseRightButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl); } //TODO: Ensure left button is checked for internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) { - KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); return UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl); } @@ -4654,7 +4654,7 @@ namespace Avalonia.Controls private bool ProcessAKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift, out bool alt); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift, out bool alt); if (ctrl && !shift && !alt && SelectionMode == DataGridSelectionMode.Extended) { @@ -4923,7 +4923,7 @@ namespace Avalonia.Controls private bool ProcessF2Key(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); if (!shift && !ctrl && _editingColumnIndex == -1 && CurrentColumnIndex != -1 && GetRowSelection(CurrentSlot) && @@ -5280,7 +5280,7 @@ namespace Avalonia.Controls private bool ProcessTabKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift); return ProcessTabKey(e, shift, ctrl); } @@ -6099,7 +6099,7 @@ namespace Avalonia.Controls /// Whether or not the DataGrid handled the key press. private bool ProcessCopyKey(KeyModifiers modifiers) { - KeyboardHelper.GetMetaKeyState(modifiers, out bool ctrl, out bool shift, out bool alt); + KeyboardHelper.GetMetaKeyState(this, modifiers, out bool ctrl, out bool shift, out bool alt); if (ctrl && !shift && !alt && ClipboardCopyMode != DataGridClipboardCopyMode.None && SelectedItems.Count > 0) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index d1e1efdd85..d28c0969c4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -703,7 +703,7 @@ namespace Avalonia.Controls public void ClearSort() { //InvokeProcessSort is already validating if sorting is possible - _headerCell?.InvokeProcessSort(KeyboardHelper.GetPlatformCtrlOrCmdKeyModifier()); + _headerCell?.InvokeProcessSort(KeyboardHelper.GetPlatformCtrlOrCmdKeyModifier(OwningGrid)); } /// @@ -1103,6 +1103,16 @@ namespace Avalonia.Controls get; set; } + + /// + /// Gets or sets an object associated with this column. + /// + public object Tag + { + get; + set; + } + /// /// Holds a Comparer to use for sorting, if not using the default. /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index ef1e84c745..0755864c25 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -241,7 +241,7 @@ namespace Avalonia.Controls DataGrid owningGrid = OwningGrid; DataGridSortDescription newSort; - KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(this, keyModifiers, out bool ctrl, out bool shift); DataGridSortDescription sort = OwningColumn.GetSortDescription(); IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; diff --git a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs index d2b1fd4b8e..bcb30888dc 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs @@ -10,23 +10,23 @@ namespace Avalonia.Controls.Utils { internal static class KeyboardHelper { - public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift) + public static void GetMetaKeyState(Control target, KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift) { - ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier()); + ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier(target)); shift = modifiers.HasFlag(KeyModifiers.Shift); } - public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift, out bool alt) + public static void GetMetaKeyState(Control target, KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift, out bool alt) { - ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier()); + ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier(target)); shift = modifiers.HasFlag(KeyModifiers.Shift); alt = modifiers.HasFlag(KeyModifiers.Alt); } - public static KeyModifiers GetPlatformCtrlOrCmdKeyModifier() - { - var keymap = AvaloniaLocator.Current.GetService(); - return keymap?.CommandModifiers ?? KeyModifiers.Control; + public static KeyModifiers GetPlatformCtrlOrCmdKeyModifier(Control target) + { + var keymap = TopLevel.GetTopLevel(target)!.PlatformSettings!.HotkeyConfiguration; + return keymap.CommandModifiers; } } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index b98ad123f1..30791551f5 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -180,6 +180,17 @@ namespace Avalonia /// public IApplicationLifetime? ApplicationLifetime { get; set; } + /// + /// Represents a contract for accessing global platform-specific settings. + /// + /// + /// PlatformSettings can be null only if application wasn't initialized yet. + /// 's is an equivalent API + /// which should always be preferred over a global one, + /// as specific top levels might have different settings set-up. + /// + public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService(); + event Action>? IGlobalStyles.GlobalStylesAdded { add => _stylesAdded += value; @@ -229,10 +240,12 @@ namespace Avalonia var focusManager = new FocusManager(); InputManager = new InputManager(); - var settings = AvaloniaLocator.Current.GetRequiredService(); - settings.ColorValuesChanged += OnColorValuesChanged; - OnColorValuesChanged(settings, settings.GetColorValues()); - + if (PlatformSettings is { } settings) + { + settings.ColorValuesChanged += OnColorValuesChanged; + OnColorValuesChanged(settings, settings.GetColorValues()); + } + AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(this) @@ -242,7 +255,7 @@ namespace Avalonia .Bind().ToConstant(InputManager) .Bind().ToTransient() .Bind().ToConstant(DragDropDevice.Instance); - + // TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x if (AvaloniaLocator.Current.GetService() == null) AvaloniaLocator.CurrentMutable diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 3dc1514667..e3a419d7b3 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -405,7 +405,7 @@ namespace Avalonia.Controls { if (IsOpen) { - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = Application.Current!.PlatformSettings!.HotkeyConfiguration; if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true && !CancelClosing()) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 8d140bdd62..6e0063c9ec 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -480,7 +480,7 @@ namespace Avalonia.Controls if (e.Source == this && !e.Handled) { - var keymap = AvaloniaLocator.Current.GetService()?.OpenContextMenu; + var keymap = Application.Current!.PlatformSettings?.HotkeyConfiguration.OpenContextMenu; if (keymap is null) { diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index f379120638..106ac8dff5 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using Avalonia.Threading; namespace Avalonia.Controls.Embedding.Offscreen { @@ -17,7 +13,6 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; - private ManualRenderTimer _manualRenderTimer = new(); public IInputRoot? InputRoot { get; private set; } public bool IsDisposed { get; private set; } @@ -27,21 +22,10 @@ namespace Avalonia.Controls.Embedding.Offscreen IsDisposed = true; } - class ManualRenderTimer : IRenderTimer - { - static Stopwatch St = Stopwatch.StartNew(); - public event Action? Tick; - public bool RunsInBackground => false; - public void TriggerTick() => Tick?.Invoke(St.Elapsed); - } - public Compositor Compositor { get; } public OffscreenTopLevelImplBase() - { - Compositor = new Compositor(new RenderLoop(_manualRenderTimer), null, false, - MediaContext.Instance, false); - } + => Compositor = new Compositor(null); public abstract IEnumerable Surfaces { get; } @@ -76,7 +60,6 @@ namespace Avalonia.Controls.Embedding.Offscreen public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } - /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 7fd9fad605..c3cf1f4897 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -9,6 +9,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.Reactive; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -392,7 +393,7 @@ namespace Avalonia.Controls.Primitives && IsOpen && Target?.ContextFlyout == this) { - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = Application.Current!.PlatformSettings?.HotkeyConfiguration; if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true) { diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f3f0b135b8..220903cc04 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -127,7 +128,7 @@ namespace Avalonia.Controls protected override void OnKeyDown(KeyEventArgs e) { - var hotkeys = AvaloniaLocator.Current.GetService(); + var hotkeys = Application.Current!.PlatformSettings?.HotkeyConfiguration; var ctrl = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers); if (!ctrl && @@ -165,7 +166,7 @@ namespace Avalonia.Controls internal bool UpdateSelectionFromPointerEvent(Control source, PointerEventArgs e) { - var hotkeys = AvaloniaLocator.Current.GetService(); + var hotkeys = Application.Current!.PlatformSettings?.HotkeyConfiguration; var toggle = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers); return UpdateSelectionFromEventSource( diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index c2044661e2..15136e5992 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right) { var point = e.GetCurrentPoint(this); - var settings = AvaloniaLocator.Current.GetService(); + var settings = TopLevel.GetTopLevel(e.Source as Visual)?.PlatformSettings; var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4); var tapRect = new Rect(_pointerDownPoint, new Size()) .Inflate(new Thickness(tapSize.Width, tapSize.Height)); diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index 4800b7b1e4..10eae5e7c0 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -7,6 +7,7 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Styling; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -204,7 +205,7 @@ namespace Avalonia.Controls return; } - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = Application.Current!.PlatformSettings?.HotkeyConfiguration; bool Match(List gestures) => gestures.Any(g => g.Matches(e)); diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index fdc098777a..c8225f775b 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -99,9 +99,15 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl continue; } - if (_nextTimer != null) + TimeSpan? nextTimer; + lock (_lock) + { + nextTimer = _nextTimer; + } + + if (nextTimer != null) { - var waitFor = _clock.Elapsed - _nextTimer.Value; + var waitFor = nextTimer.Value - _clock.Elapsed; if (waitFor.TotalMilliseconds < 1) continue; _wakeup.WaitOne(waitFor); @@ -112,4 +118,4 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl registration.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 2236d87dd1..5dd0f23e8e 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -120,11 +120,11 @@ namespace Avalonia.Controls.Primitives void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize) { _lastRequestedPosition = devicePoint; - Dispatcher.UIThread.Post(() => + MediaContext.Instance.BeginInvokeOnRender(() => { - OverlayLayer.SetLeft(this, _lastRequestedPosition.X); - OverlayLayer.SetTop(this, _lastRequestedPosition.Y); - }, DispatcherPriority.Render); + Canvas.SetLeft(this, _lastRequestedPosition.X); + Canvas.SetTop(this, _lastRequestedPosition.Y); + }); } double IManagedPopupPositionerPopup.Scaling => 1; diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index c5d73dc00f..7299300cc7 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -113,6 +113,8 @@ namespace Avalonia.Controls.Primitives PseudoClasses.Add(":pressed"); + e.PreventGestureRecognition(); + RaiseEvent(ev); } diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 5c7071a161..39ff8e3a92 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -238,7 +238,6 @@ namespace Avalonia.Controls visualizerVisual.Offset = IsPullDirectionVertical ? new Vector3(visualizerVisual.Offset.X, 0, 0) : new Vector3(0, visualizerVisual.Offset.Y, 0); - visual.Offset = default; _content.InvalidateMeasure(); break; case RefreshVisualizerState.Interacting: @@ -452,8 +451,6 @@ namespace Avalonia.Controls _interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) .Subscribe(InteractionRatioObserver); - var visual = RefreshInfoProvider.Visual; - _executingRatio = RefreshInfoProvider.ExecutionRatio; } else diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs new file mode 100644 index 0000000000..593adfb225 --- /dev/null +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.Framebuffer.cs @@ -0,0 +1,116 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Avalonia.Platform; +using Avalonia.Remote.Protocol.Viewport; +using PlatformPixelFormat = Avalonia.Platform.PixelFormat; +using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat; + +namespace Avalonia.Controls.Remote.Server +{ + internal partial class RemoteServerTopLevelImpl + { + private enum FrameStatus + { + NotRendered, + Rendered, + CopiedToMessage + } + + private sealed class Framebuffer + { + public static Framebuffer Empty { get; } = new(ProtocolPixelFormat.Rgba8888, default, 1.0); + + private readonly double _dpi; + private readonly PixelSize _frameSize; + private readonly object _dataLock = new(); + private readonly byte[] _data; // for rendering only + private readonly byte[] _dataCopy; // for messages only + private FrameStatus _status = FrameStatus.NotRendered; + + public Framebuffer(ProtocolPixelFormat format, Size clientSize, double renderScaling) + { + var frameSize = PixelSize.FromSize(clientSize, renderScaling); + if (frameSize.Width <= 0 || frameSize.Height <= 0) + frameSize = PixelSize.Empty; + + var bpp = format == ProtocolPixelFormat.Rgb565 ? 2 : 4; + var stride = frameSize.Width * bpp; + var dataLength = Math.Max(0, stride * frameSize.Height); + + _dpi = renderScaling * 96.0; + _frameSize = frameSize; + Format = format; + ClientSize = clientSize; + RenderScaling = renderScaling; + + (Stride, _data, _dataCopy) = dataLength > 0 ? + (stride, new byte[dataLength], new byte[dataLength]) : + (0, Array.Empty(), Array.Empty()); + } + + public ProtocolPixelFormat Format { get; } + + public Size ClientSize { get; } + + public double RenderScaling { get; } + + public int Stride { get; } + + public FrameStatus GetStatus() + { + lock (_dataLock) + return _status; + } + + public ILockedFramebuffer Lock(Action onUnlocked) + { + var handle = GCHandle.Alloc(_data, GCHandleType.Pinned); + Monitor.Enter(_dataLock); + + try + { + return new LockedFramebuffer( + handle.AddrOfPinnedObject(), + _frameSize, + Stride, + new Vector(_dpi, _dpi), + new PlatformPixelFormat((PixelFormatEnum)Format), + () => + { + handle.Free(); + Array.Copy(_data, _dataCopy, _data.Length); + _status = FrameStatus.Rendered; + Monitor.Exit(_dataLock); + onUnlocked(); + }); + } + catch + { + handle.Free(); + Monitor.Exit(_dataLock); + throw; + } + } + + /// The returned message must NOT be kept around, as it contains a shared buffer. + public FrameMessage ToMessage(long sequenceId) + { + lock (_dataLock) + _status = FrameStatus.CopiedToMessage; + + return new FrameMessage + { + SequenceId = sequenceId, + Data = _dataCopy, + Format = Format, + Width = _frameSize.Width, + Height = _frameSize.Height, + Stride = Stride, + DpiX = _dpi, + DpiY = _dpi + }; + } + } + } +} diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 8e4123a790..49af6a71a0 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; using Avalonia.Controls.Embedding.Offscreen; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; @@ -11,7 +10,6 @@ using Avalonia.Platform; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Input; using Avalonia.Remote.Protocol.Viewport; -using Avalonia.Rendering; using Avalonia.Threading; using Key = Avalonia.Input.Key; using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat; @@ -20,28 +18,28 @@ using ProtocolMouseButton = Avalonia.Remote.Protocol.Input.MouseButton; namespace Avalonia.Controls.Remote.Server { [Unstable] - internal class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl + internal partial class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl { private readonly IAvaloniaRemoteTransportConnection _transport; - private LockedFramebuffer? _framebuffer; private readonly object _lock = new(); + private readonly Action _sendLastFrameIfNeeded; + private readonly Action _renderAndSendFrameIfNeeded; + private Framebuffer _framebuffer = Framebuffer.Empty; private long _lastSentFrame = -1; private long _lastReceivedFrame = -1; private long _nextFrameNumber = 1; private ClientViewportAllocatedMessage? _pendingAllocation; - private bool _queuedNextRender; - private bool _inRender; - private Vector _dpi = new Vector(96, 96); - private ProtocolPixelFormat[]? _supportedFormats; + private ProtocolPixelFormat? _format; public RemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport) { + _sendLastFrameIfNeeded = SendLastFrameIfNeeded; + _renderAndSendFrameIfNeeded = RenderAndSendFrameIfNeeded; + _transport = transport; _transport.OnMessage += OnMessage; KeyboardDevice = AvaloniaLocator.Current.GetRequiredService(); - QueueNextRender(); - Compositor.AfterCommit += QueueNextRender; } private static RawPointerEventType GetAvaloniaEventType(ProtocolMouseButton button, bool pressed) @@ -112,45 +110,41 @@ namespace Avalonia.Controls.Remote.Server { lock (_lock) { - if (obj is FrameReceivedMessage lastFrame) + switch (obj) { - lock (_lock) - { + case FrameReceivedMessage lastFrame: _lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame); - } - Dispatcher.UIThread.Post(RenderIfNeeded); - } - if(obj is ClientRenderInfoMessage renderInfo) - { - lock(_lock) - { - _dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY); - _queuedNextRender = true; - } - - Dispatcher.UIThread.Post(RenderIfNeeded); - } - if (obj is ClientSupportedPixelFormatsMessage supportedFormats) - { - lock (_lock) - _supportedFormats = supportedFormats.Formats; - Dispatcher.UIThread.Post(RenderIfNeeded); - } - if (obj is MeasureViewportMessage measure) - Dispatcher.UIThread.Post(() => - { - var m = Measure(new Size(measure.Width, measure.Height)); - _transport.Send(new MeasureViewportMessage + Dispatcher.UIThread.Post(_sendLastFrameIfNeeded); + break; + + case ClientRenderInfoMessage renderInfo: + Dispatcher.UIThread.Post(() => { - Width = m.Width, - Height = m.Height + RenderScaling = renderInfo.DpiX / 96.0; + RenderAndSendFrameIfNeeded(); }); - }); - if (obj is ClientViewportAllocatedMessage allocated) - { - lock (_lock) - { + break; + + case ClientSupportedPixelFormatsMessage supportedFormats: + _format = TryGetValidPixelFormat(supportedFormats.Formats); + Dispatcher.UIThread.Post(_renderAndSendFrameIfNeeded); + break; + + case MeasureViewportMessage measure: + Dispatcher.UIThread.Post(() => + { + var m = Measure(new Size(measure.Width, measure.Height)); + _transport.Send(new MeasureViewportMessage + { + Width = m.Width, + Height = m.Height + }); + }); + break; + + case ClientViewportAllocatedMessage allocated: if (_pendingAllocation == null) + { Dispatcher.UIThread.Post(() => { ClientViewportAllocatedMessage allocation; @@ -159,101 +153,111 @@ namespace Avalonia.Controls.Remote.Server allocation = _pendingAllocation!; _pendingAllocation = null; } - _dpi = new Vector(allocation.DpiX, allocation.DpiY); + + RenderScaling = allocation.DpiX / 96.0; ClientSize = new Size(allocation.Width, allocation.Height); - RenderIfNeeded(); + RenderAndSendFrameIfNeeded(); }); + } _pendingAllocation = allocated; - } - } - if(obj is PointerMovedEventMessage pointer) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawPointerEventArgs( - MouseDevice, - 0, - InputRoot!, - RawPointerEventType.Move, - new Point(pointer.X, pointer.Y), - GetAvaloniaRawInputModifiers(pointer.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is PointerPressedEventMessage pressed) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawPointerEventArgs( - MouseDevice, - 0, - InputRoot!, - GetAvaloniaEventType(pressed.Button, true), - new Point(pressed.X, pressed.Y), - GetAvaloniaRawInputModifiers(pressed.Modifiers))); - }, DispatcherPriority.Input); - } - if (obj is PointerReleasedEventMessage released) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawPointerEventArgs( - MouseDevice, - 0, - InputRoot!, - GetAvaloniaEventType(released.Button, false), - new Point(released.X, released.Y), - GetAvaloniaRawInputModifiers(released.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is ScrollEventMessage scroll) - { - Dispatcher.UIThread.Post(() => - { - Input?.Invoke(new RawMouseWheelEventArgs( - MouseDevice, - 0, - InputRoot!, - new Point(scroll.X, scroll.Y), - new Vector(scroll.DeltaX, scroll.DeltaY), - GetAvaloniaRawInputModifiers(scroll.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is KeyEventMessage key) - { - Dispatcher.UIThread.Post(() => - { - Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - - Input?.Invoke(new RawKeyEventArgs( - KeyboardDevice, - 0, - InputRoot!, - key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - (Key)key.Key, - GetAvaloniaRawInputModifiers(key.Modifiers))); - }, DispatcherPriority.Input); - } - if(obj is TextInputEventMessage text) - { - Dispatcher.UIThread.Post(() => - { - Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - - Input?.Invoke(new RawTextInputEventArgs( - KeyboardDevice, - 0, - InputRoot!, - text.Text)); - }, DispatcherPriority.Input); + break; + + case PointerMovedEventMessage pointer: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawPointerEventArgs( + MouseDevice, + 0, + InputRoot!, + RawPointerEventType.Move, + new Point(pointer.X, pointer.Y), + GetAvaloniaRawInputModifiers(pointer.Modifiers))); + }, DispatcherPriority.Input); + break; + + case PointerPressedEventMessage pressed: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawPointerEventArgs( + MouseDevice, + 0, + InputRoot!, + GetAvaloniaEventType(pressed.Button, true), + new Point(pressed.X, pressed.Y), + GetAvaloniaRawInputModifiers(pressed.Modifiers))); + }, DispatcherPriority.Input); + break; + + case PointerReleasedEventMessage released: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawPointerEventArgs( + MouseDevice, + 0, + InputRoot!, + GetAvaloniaEventType(released.Button, false), + new Point(released.X, released.Y), + GetAvaloniaRawInputModifiers(released.Modifiers))); + }, DispatcherPriority.Input); + break; + + case ScrollEventMessage scroll: + Dispatcher.UIThread.Post(() => + { + Input?.Invoke(new RawMouseWheelEventArgs( + MouseDevice, + 0, + InputRoot!, + new Point(scroll.X, scroll.Y), + new Vector(scroll.DeltaX, scroll.DeltaY), + GetAvaloniaRawInputModifiers(scroll.Modifiers))); + }, DispatcherPriority.Input); + break; + + case KeyEventMessage key: + Dispatcher.UIThread.Post(() => + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + + Input?.Invoke(new RawKeyEventArgs( + KeyboardDevice, + 0, + InputRoot!, + key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + (Key)key.Key, + GetAvaloniaRawInputModifiers(key.Modifiers))); + }, DispatcherPriority.Input); + break; + + case TextInputEventMessage text: + Dispatcher.UIThread.Post(() => + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + + Input?.Invoke(new RawTextInputEventArgs( + KeyboardDevice, + 0, + InputRoot!, + text.Text)); + }, DispatcherPriority.Input); + break; } } } - protected void SetDpi(Vector dpi) + private static ProtocolPixelFormat? TryGetValidPixelFormat(ProtocolPixelFormat[]? formats) { - _dpi = dpi; - RenderIfNeeded(); + if (formats is not null) + { + foreach (var format in formats) + { + if (format is >= 0 and <= ProtocolPixelFormat.MaxValue) + return format; + } + } + + return null; } protected virtual Size Measure(Size constraint) @@ -265,88 +269,63 @@ namespace Avalonia.Controls.Remote.Server public override IEnumerable Surfaces => new[] { this }; - private FrameMessage RenderFrame(int width, int height, ProtocolPixelFormat? format) + private Framebuffer GetOrCreateFramebuffer() { - var scalingX = _dpi.X / 96.0; - var scalingY = _dpi.Y / 96.0; - - width = (int)(width * scalingX); - height = (int)(height * scalingY); - - var fmt = format ?? ProtocolPixelFormat.Rgba8888; - var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4; - var data = new byte[width * height * bpp]; - var handle = GCHandle.Alloc(data, GCHandleType.Pinned); - - try - { - if (width > 0 && height > 0) - { - _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, new((PixelFormatEnum)fmt), - null); - Paint?.Invoke(new Rect(0, 0, width, height)); - } - } - finally + lock (_lock) { - _framebuffer = null; - handle.Free(); + if (_format is not { } format) + _framebuffer = Framebuffer.Empty; + else if (_framebuffer.Format != format || _framebuffer.ClientSize != ClientSize || _framebuffer.RenderScaling != RenderScaling) + _framebuffer = new Framebuffer(format, ClientSize, RenderScaling); + + return _framebuffer; } - return new FrameMessage - { - Data = data, - Format = fmt, - Width = width, - Height = height, - Stride = width * bpp, - DpiX = _dpi.X, - DpiY = _dpi.Y - }; } public ILockedFramebuffer Lock() - { - if (_framebuffer == null) - throw new InvalidOperationException("Paint was not requested, wait for Paint event"); - return _framebuffer; - } + => GetOrCreateFramebuffer().Lock(_sendLastFrameIfNeeded); - protected void RenderIfNeeded() + private void SendLastFrameIfNeeded() { - lock (_lock) - { - if (_lastReceivedFrame != _lastSentFrame || !_queuedNextRender || _supportedFormats == null) - return; + if (IsDisposed) + return; - } + Framebuffer framebuffer; + long sequenceId; - var format = ProtocolPixelFormat.Rgba8888; - foreach(var fmt in _supportedFormats) - if (fmt <= ProtocolPixelFormat.MaxValue) - { - format = fmt; - break; - } - - _inRender = true; - var frame = RenderFrame((int) ClientSize.Width, (int) ClientSize.Height, format); lock (_lock) { + // Ideally we should only send a frame if its status is Rendered: since the renderer might not be + // initialized at the start, we're sending black frames in this case. However, this was the historical + // behavior and some external programs are depending on receiving a frame asap. + if (_lastReceivedFrame != _lastSentFrame || _framebuffer.GetStatus() == FrameStatus.CopiedToMessage) + return; + + framebuffer = _framebuffer; _lastSentFrame = _nextFrameNumber++; - frame.SequenceId = _lastSentFrame; - _queuedNextRender = false; + sequenceId = _lastSentFrame; } - _inRender = false; - _transport.Send(frame); + + _transport.Send(framebuffer.ToMessage(sequenceId)); } - private void QueueNextRender() + protected void RenderAndSendFrameIfNeeded() { - if (!_inRender && !IsDisposed) + if (IsDisposed) + return; + + lock (_lock) { - _queuedNextRender = true; - DispatcherTimer.RunOnce(RenderIfNeeded, TimeSpan.FromMilliseconds(2), DispatcherPriority.Background); + if (_lastReceivedFrame != _lastSentFrame || _format is null) + return; } + + var framebuffer = GetOrCreateFramebuffer(); + + if (framebuffer.Stride > 0) + Paint?.Invoke(new Rect(framebuffer.ClientSize)); + + SendLastFrameIfNeeded(); } public override IMouseDevice MouseDevice { get; } = new MouseDevice(); diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 8b32638835..5e21681024 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -198,7 +198,7 @@ namespace Avalonia.Controls var handled = false; var modifiers = e.KeyModifiers; - var keymap = AvaloniaLocator.Current.GetRequiredService(); + var keymap = Application.Current!.PlatformSettings!.HotkeyConfiguration; bool Match(List gestures) => gestures.Any(g => g.Matches(e)); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 7bc26bf3b0..3016dc8239 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -15,7 +15,6 @@ using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Controls.Metadata; using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Automation.Peers; using Avalonia.Threading; @@ -32,20 +31,17 @@ namespace Avalonia.Controls /// /// Gets a platform-specific for the Cut action /// - public static KeyGesture? CutGesture { get; } = AvaloniaLocator.Current - .GetService()?.Cut.FirstOrDefault(); + public static KeyGesture? CutGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Cut.FirstOrDefault(); /// /// Gets a platform-specific for the Copy action /// - public static KeyGesture? CopyGesture { get; } = AvaloniaLocator.Current - .GetService()?.Copy.FirstOrDefault(); + public static KeyGesture? CopyGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Copy.FirstOrDefault(); /// /// Gets a platform-specific for the Paste action /// - public static KeyGesture? PasteGesture { get; } = AvaloniaLocator.Current - .GetService()?.Paste.FirstOrDefault(); + public static KeyGesture? PasteGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Paste.FirstOrDefault(); /// /// Defines the property @@ -1103,7 +1099,7 @@ namespace Avalonia.Controls var handled = false; var modifiers = e.KeyModifiers; - var keymap = AvaloniaLocator.Current.GetRequiredService(); + var keymap = Application.Current!.PlatformSettings!.HotkeyConfiguration; bool Match(List gestures) => gestures.Any(g => g.Matches(e)); bool DetectSelection() => e.KeyModifiers.HasAllFlags(keymap.SelectionModifiers); @@ -1217,6 +1213,34 @@ namespace Avalonia.Controls selection = true; handled = true; } + else if (Match(keymap.PageLeft)) + { + MovePageLeft(); + movement = true; + selection = false; + handled = true; + } + else if (Match(keymap.PageRight)) + { + MovePageRight(); + movement = true; + selection = false; + handled = true; + } + else if (Match(keymap.PageUp)) + { + MovePageUp(); + movement = true; + selection = false; + handled = true; + } + else if (Match(keymap.PageDown)) + { + MovePageDown(); + movement = true; + selection = false; + handled = true; + } else { bool hasWholeWordModifiers = modifiers.HasAllFlags(keymap.WholeWordTextActionModifiers); @@ -1407,8 +1431,6 @@ namespace Avalonia.Controls { var point = e.GetPosition(_presenter); - var oldIndex = CaretIndex; - _presenter.MoveCaretToPoint(point); var caretIndex = _presenter.CaretIndex; @@ -1741,6 +1763,25 @@ namespace Avalonia.Controls } } + private void MovePageRight() + { + _scrollViewer?.PageRight(); + } + + private void MovePageLeft() + { + _scrollViewer?.PageLeft(); + } + private void MovePageUp() + { + _scrollViewer?.PageUp(); + } + + private void MovePageDown() + { + _scrollViewer?.PageDown(); + } + /// /// Select all text in the TextBox /// diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index a68a022e67..48b068d324 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Metadata; +using Avalonia.Animation; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -42,6 +43,10 @@ namespace Avalonia.Controls x.UpdateKnobPos(x.IsChecked.Value); } }); + KnobTransitionsProperty.Changed.AddClassHandler((x, e) => + { + x.UpdateKnobTransitions(); + }); } /// @@ -68,6 +73,12 @@ namespace Avalonia.Controls public static readonly StyledProperty OnContentTemplateProperty = AvaloniaProperty.Register(nameof(OnContentTemplate)); + /// + /// Defines the property. + /// + public static readonly StyledProperty KnobTransitionsProperty = + AvaloniaProperty.Register(nameof(KnobTransitions)); + /// /// Gets or Sets the Content that is displayed when in the On State. /// @@ -116,6 +127,17 @@ namespace Avalonia.Controls set { SetValue(OnContentTemplateProperty, value); } } + /// + /// Gets or Sets the of switching knob. + /// + public Transitions KnobTransitions + { + get { return GetValue(KnobTransitionsProperty); } + set { SetValue(KnobTransitionsProperty, value); } + } + + + private void OffContentChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) @@ -177,7 +199,21 @@ namespace Avalonia.Controls UpdateKnobPos(IsChecked.Value); } } - + + protected override void OnLoaded() + { + base.OnLoaded(); + UpdateKnobTransitions(); + } + + private void UpdateKnobTransitions() + { + if (_knobsPanel != null) + { + _knobsPanel.Transitions = KnobTransitions; + } + } + private void KnobsPanel_PointerPressed(object? sender, Input.PointerPressedEventArgs e) { _switchStartPoint = e.GetPosition(_switchKnob); @@ -194,7 +230,7 @@ namespace Avalonia.Controls _knobsPanel!.ClearValue(Canvas.LeftProperty); PseudoClasses.Set(":dragging", false); - + if (shouldBecomeChecked == IsChecked) { UpdateKnobPos(shouldBecomeChecked); @@ -203,6 +239,7 @@ namespace Avalonia.Controls { SetCurrentValue(IsCheckedProperty, shouldBecomeChecked); } + UpdateKnobTransitions(); } else { @@ -218,6 +255,10 @@ namespace Avalonia.Controls { if (_knobsPanelPressed) { + if(_knobsPanel != null) + { + _knobsPanel.Transitions = null; + } var difference = e.GetPosition(_switchKnob) - _switchStartPoint; if ((!_isDragging) && (System.Math.Abs(difference.X) > 3)) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 92444b9c07..04a5a0e6aa 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -25,6 +25,7 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Metadata; using Avalonia.Rendering.Composition; +using Avalonia.Threading; namespace Avalonia.Controls { @@ -250,7 +251,7 @@ namespace Avalonia.Controls if (e is RawKeyEventArgs rawKeyEventArgs && rawKeyEventArgs.Type == RawKeyEventType.KeyDown) { - var keymap = AvaloniaLocator.Current.GetService()?.Back; + var keymap = PlatformSettings?.HotkeyConfiguration.Back; if (keymap != null) { @@ -487,6 +488,9 @@ namespace Avalonia.Controls /// public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService(); + /// + public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService(); + /// Point IRenderRoot.PointToClient(PixelPoint p) { @@ -532,7 +536,16 @@ namespace Avalonia.Controls return Disposable.Create(() => { }); } } - + + /// + /// Enqueues a callback to be called on the next animation tick + /// + public void RequestAnimationFrame(Action action) + { + Dispatcher.UIThread.VerifyAccess(); + MediaContext.Instance.RequestAnimationFrame(action); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index a499829d4a..7bf8d3bb68 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -571,7 +571,7 @@ namespace Avalonia.Controls if (!e.Handled) { - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = Application.Current!.PlatformSettings!.HotkeyConfiguration; bool Match(List? gestures) => gestures?.Any(g => g.Matches(e)) ?? false; if (this.SelectionMode == SelectionMode.Multiple && Match(keymap?.SelectAll)) @@ -652,11 +652,12 @@ namespace Avalonia.Controls if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) { + var keymap = Application.Current!.PlatformSettings!.HotkeyConfiguration; e.Handled = UpdateSelectionFromEventSource( e.Source, true, e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), - e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService().CommandModifiers), + e.KeyModifiers.HasAllFlags(keymap.CommandModifiers), point.Properties.IsRightButtonPressed); } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index e0fcf8e530..b6c0c3ae3d 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -53,7 +53,11 @@ namespace Avalonia.DesignerSupport.Remote // In previewer mode we completely ignore client-side viewport size if (obj is ClientViewportAllocatedMessage alloc) { - Dispatcher.UIThread.Post(() => SetDpi(new Vector(alloc.DpiX, alloc.DpiY))); + Dispatcher.UIThread.Post(() => + { + RenderScaling = alloc.DpiX / 96.0; + RenderAndSendFrameIfNeeded(); + }); return; } base.OnMessage(transport, obj); @@ -67,7 +71,7 @@ namespace Avalonia.DesignerSupport.Remote Height = clientSize.Height }); ClientSize = clientSize; - RenderIfNeeded(); + RenderAndSendFrameIfNeeded(); } public void Move(PixelPoint point) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index d2787e73ae..ba9dd592ce 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -52,7 +52,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(Keyboard) .Bind().ToSingleton() .Bind().ToConstant(new ManagedDispatcherImpl(null)) - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new UiThreadRenderTimer(60)) .Bind().ToConstant(instance) .Bind().ToSingleton() .Bind().ToSingleton(); diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 9abf4f97a7..61e46b651e 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -60,9 +60,9 @@ namespace Avalonia.FreeDesktop { try { - _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2)); + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item1, x.Item3)); var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); - OnNameChange(nameOwner); + OnNameChange("org.kde.StatusNotifierWatcher", nameOwner); } catch { @@ -72,9 +72,9 @@ namespace Avalonia.FreeDesktop } } - private void OnNameChange(string? newOwner) + private void OnNameChange(string name, string? newOwner) { - if (_isDisposed || _connection is null) + if (_isDisposed || _connection is null || name != "org.kde.StatusNotifierWatcher") return; if (!_serviceConnected & newOwner is not null) diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index c471cf91d2..b32382084d 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -108,20 +108,18 @@ namespace Avalonia.Native .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind().ToConstant(new DefaultRenderTimer(60)) - .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) .Bind().ToConstant(applicationPlatform) .Bind().ToConstant(new MacOSNativeMenuCommands(_factory.CreateApplicationCommands())); - - var hotkeys = AvaloniaLocator.Current.GetService(); - if (hotkeys is not null) - { - hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); - hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); - hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); - hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); - } + + var hotkeys = new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt); + hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(hotkeys); if (_options.UseGpu) { diff --git a/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs index a4ef15f950..a796a5704d 100644 --- a/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs +++ b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs @@ -16,18 +16,12 @@ internal class SystemAccentColors : IResourceProvider public const string AccentLight2Key = "SystemAccentColorLight2"; public const string AccentLight3Key = "SystemAccentColorLight3"; - private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215); - private readonly IPlatformSettings? _platformSettings; + private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215); private bool _invalidateColors = true; private Color _systemAccentColor; private Color _systemAccentColorDark1, _systemAccentColorDark2, _systemAccentColorDark3; private Color _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3; - public SystemAccentColors() - { - _platformSettings = AvaloniaLocator.Current.GetService(); - } - public bool HasResources => true; public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { @@ -96,10 +90,12 @@ internal class SystemAccentColors : IResourceProvider Owner = owner; OwnerChanged?.Invoke(this, EventArgs.Empty); - if (_platformSettings is not null) + if (GetFromOwner(owner) is { } platformSettings) { - _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; + platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; } + + _invalidateColors = true; } } @@ -110,10 +106,12 @@ internal class SystemAccentColors : IResourceProvider Owner = null; OwnerChanged?.Invoke(this, EventArgs.Empty); - if (_platformSettings is not null) + if (GetFromOwner(owner) is { } platformSettings) { - _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; + platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; } + + _invalidateColors = true; } } @@ -122,13 +120,25 @@ internal class SystemAccentColors : IResourceProvider if (_invalidateColors) { _invalidateColors = false; + + var platformSettings = GetFromOwner(Owner); - _systemAccentColor = _platformSettings?.GetColorValues().AccentColor1 ?? s_defaultSystemAccentColor; + _systemAccentColor = platformSettings?.GetColorValues().AccentColor1 ?? s_defaultSystemAccentColor; (_systemAccentColorDark1,_systemAccentColorDark2, _systemAccentColorDark3, _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3) = CalculateAccentShades(_systemAccentColor); } } + private static IPlatformSettings? GetFromOwner(IResourceHost? owner) + { + return owner switch + { + Application app => app.PlatformSettings, + Visual visual => TopLevel.GetTopLevel(visual)?.PlatformSettings, + _ => null + }; + } + public static (Color d1, Color d2, Color d3, Color l1, Color l2, Color l3) CalculateAccentShades(Color accentColor) { // dark1step = (hslAccent.L - SystemAccentColorDark1.L) * 255 diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml index 47d7953096..ff9de247f4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml @@ -28,6 +28,14 @@ + + + + + @@ -134,18 +142,6 @@ - - -