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/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/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index 8868e966f0..04c444f441 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -51,7 +51,7 @@ namespace Avalonia.Input pointer.Capture(hit); } - var target = pointer.Captured ?? args.Root; + var target = pointer.Captured ?? args.InputHitTestResult ?? args.Root; var gestureTarget = pointer.CapturedGestureRecognizer?.Target; var updateKind = args.Type.ToUpdateKind(); var keyModifier = args.InputModifiers.ToKeyModifiers(); 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/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 350d8817f1..0f70386424 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -643,12 +643,13 @@ namespace Avalonia.Media lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster; } + var isReversed = firstCluster > lastCluster; + if (!IsLeftToRight) { (lastCluster, firstCluster) = (firstCluster, lastCluster); } - var isReversed = firstCluster > lastCluster; var height = GlyphTypeface.Metrics.LineSpacing * Scale; var widthIncludingTrailingWhitespace = 0d; @@ -766,15 +767,13 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - var characterIndex = 0; + var characterIndex = charactersSpan.Length - 1; for (var i = 0; i < _glyphInfos.Count; i++) { var currentCluster = _glyphInfos[i].GlyphCluster; var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - characterIndex += characterLength; - if (!codepoint.IsWhiteSpace) { break; @@ -784,9 +783,9 @@ namespace Avalonia.Media var j = i; - while (j - 1 >= 0) + while (j + 1 < _glyphInfos.Count) { - var nextCluster = _glyphInfos[--j].GlyphCluster; + var nextCluster = _glyphInfos[++j].GlyphCluster; if (currentCluster == nextCluster) { @@ -798,6 +797,8 @@ namespace Avalonia.Media break; } + characterIndex -= clusterLength; + if (codepoint.IsBreakChar) { newLineLength += clusterLength; 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 05872a3e50..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; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index a609800fb8..d2198a2cbf 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -684,7 +684,9 @@ namespace Avalonia.Media.TextFormatting var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); + line.FinalizeLine(); + return line; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4ccb3f6a37..b6b6d11a49 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting /// /// Gets the text spacing. /// - public double LetterSpacing => _paragraphProperties.LetterSpacing; + public double LetterSpacing => _paragraphProperties.LetterSpacing; /// /// Gets the text lines. @@ -271,11 +271,13 @@ namespace Avalonia.Media.TextFormatting var currentY = 0.0; - foreach (var textLine in _textLines) + for (var i = 0; i < _textLines.Length; i++) { + var textLine = _textLines[i]; + var end = textLine.FirstTextSourceIndex + textLine.Length; - if (end <= textPosition && end < _textSourceLength) + if (end <= textPosition && i + 1 < _textLines.Length) { currentY += textLine.Height; @@ -511,7 +513,7 @@ namespace Avalonia.Media.TextFormatting { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); - UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, + UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); return new TextLine[] { textLine }; @@ -638,13 +640,13 @@ namespace Avalonia.Media.TextFormatting } private void UpdateMetrics( - TextLine currentLine, - ref double lineStartOfLongestLine, - ref Point origin, - ref bool first, + TextLine currentLine, + ref double lineStartOfLongestLine, + ref Point origin, + ref bool first, ref double accBlackBoxLeft, - ref double accBlackBoxTop, - ref double accBlackBoxRight, + ref double accBlackBoxTop, + ref double accBlackBoxRight, ref double accBlackBoxBottom) { var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index c2ec78e187..ca31d9a6d0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -371,14 +371,16 @@ namespace Avalonia.Media.TextFormatting IndexedTextRun currentIndexedRun = _indexedTextRuns[i]; - while(currentIndexedRun.TextSourceCharacterIndex != currentPosition) + while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) { - if(i + 1 < _indexedTextRuns.Count) + if (i + 1 == _indexedTextRuns.Count) { - i++; - - currentIndexedRun = _indexedTextRuns[i]; + break; } + + i++; + + currentIndexedRun = _indexedTextRuns[i]; } return currentIndexedRun; @@ -430,7 +432,7 @@ namespace Avalonia.Media.TextFormatting if (currentTextRun == null) { - return 0; + return Start; } var directionalWidth = 0.0; @@ -584,6 +586,8 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; + TextBounds? lastBounds = null; + static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection) { if (textRun is ShapedTextRun shapedTextRun) @@ -604,12 +608,14 @@ namespace Avalonia.Media.TextFormatting while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) { - if (i + 1 < _indexedTextRuns.Count) + if (i + 1 == _indexedTextRuns.Count) { - i++; - - currentIndexedRun = _indexedTextRuns[i]; + break; } + + i++; + + currentIndexedRun = _indexedTextRuns[i]; } return currentIndexedRun; @@ -632,6 +638,40 @@ namespace Avalonia.Media.TextFormatting return distance; } + bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds) + { + if (currentBounds.FlowDirection != lastBounds.FlowDirection) + { + return false; + } + + if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right) + { + foreach (var runBounds in currentBounds.TextRunBounds) + { + lastBounds.TextRunBounds.Add(runBounds); + } + + lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); + + return true; + } + + if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left) + { + for (int i = 0; i < currentBounds.TextRunBounds.Count; i++) + { + lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]); + } + + lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); + + return true; + } + + return false; + } + while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length) { var currentIndexedRun = FindIndexedRun(); @@ -667,67 +707,21 @@ namespace Avalonia.Media.TextFormatting directionalWidth = currentDrawable.Size.Width; } - if (currentTextRun is not TextEndOfLine) - { - if (currentDirection == FlowDirection.LeftToRight) - { - // Find consecutive runs of same direction - for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++) - { - var nextRun = _textRuns[lastRunIndex + 1]; - - var nextDirection = GetDirection(nextRun, currentDirection); - - if (currentDirection != nextDirection) - { - break; - } - - if (nextRun is DrawableTextRun nextDrawable) - { - directionalWidth += nextDrawable.Size.Width; - } - } - } - else - { - // Find consecutive runs of same direction - for (; firstRunIndex - 1 > 0; firstRunIndex--) - { - var previousRun = _textRuns[firstRunIndex - 1]; - - var previousDirection = GetDirection(previousRun, currentDirection); - - if (currentDirection != previousDirection) - { - break; - } - - if (previousRun is DrawableTextRun previousDrawable) - { - directionalWidth += previousDrawable.Size.Width; - - currentX -= previousDrawable.Size.Width; - } - } - } - } - int coveredLength; - TextBounds? textBounds; + TextBounds? currentBounds; switch (currentDirection) { case FlowDirection.RightToLeft: { - textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex, + currentBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex, currentPosition, remainingLength, out coveredLength, out currentPosition); break; } default: { - textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, currentPosition, remainingLength, out coveredLength, out currentPosition); break; @@ -736,7 +730,18 @@ namespace Avalonia.Media.TextFormatting if (coveredLength > 0) { - result.Add(textBounds); + if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) + { + currentBounds = lastBounds; + + result[result.Count - 1] = currentBounds; + } + else + { + result.Add(currentBounds); + } + + lastBounds = currentBounds; remainingLength -= coveredLength; } @@ -997,14 +1002,14 @@ namespace Avalonia.Media.TextFormatting public void FinalizeLine() { + _indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex); + _textLineMetrics = CreateLineMetrics(); if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) { _textLineBreak = new TextLineBreak(textEndOfLine); - } - - _indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex); + } } /// 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/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/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/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.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs index aaf272c6d2..4700081e6b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs @@ -266,26 +266,26 @@ namespace Avalonia.Controls MidnightBlue9 = 0xFF1C2833, MidnightBlue10 = 0xFF17202A, - Pomegranate = Pomegranate3, - Alizarin = Alizarin3, - Amethyst = Amethyst3, - Wisteria = Wisteria3, - BelizeHole = BelizeHole3, - PeterRiver = PeterRiver3, - Turquoise = Turquoise3, - GreenSea = GreenSea3, - Nephritis = Nephritis3, - Emerald = Emerald3, - Sunflower = Sunflower3, - Orange = Orange3, - Carrot = Carrot3, - Pumpkin = Pumpkin3, - Clouds = Clouds3, - Silver = Silver3, - Concrete = Concrete3, - Asbestos = Asbestos3, - WetAsphalt = WetAsphalt3, - MidnightBlue = MidnightBlue3, + Pomegranate = Pomegranate6, + Alizarin = Alizarin6, + Amethyst = Amethyst6, + Wisteria = Wisteria6, + BelizeHole = BelizeHole6, + PeterRiver = PeterRiver6, + Turquoise = Turquoise6, + GreenSea = GreenSea6, + Nephritis = Nephritis6, + Emerald = Emerald6, + Sunflower = Sunflower6, + Orange = Orange6, + Carrot = Carrot6, + Pumpkin = Pumpkin6, + Clouds = Clouds6, + Silver = Silver6, + Concrete = Concrete6, + Asbestos = Asbestos6, + WetAsphalt = WetAsphalt6, + MidnightBlue = MidnightBlue6, }; // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 6683346eeb..deb0dfb6dd 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -45,7 +45,6 @@ namespace Avalonia.Controls.Primitives private bool _updatingColor = false; private bool _updatingHsvColor = false; - private bool _coercedInitialColor = false; private bool _isPointerPressed = false; private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); @@ -622,7 +621,7 @@ namespace Avalonia.Controls.Primitives // that no color has been selected by the user. Note that #00000000 is different than // #00FFFFFF (Transparent). // - // In this situation, the first time the user clicks on the spectrum the third + // In this situation, whenever the user clicks on the spectrum, the third // component and alpha component will remain zero. This is because the spectrum only // controls two components at any given time. // @@ -633,16 +632,19 @@ namespace Avalonia.Controls.Primitives // though the desired value is simply full color. // // To work around this usability issue with an initial #00000000 color, the selected - // color is coerced (only the first time) into a color with maximum third component - // value and maximum alpha. This can only happen once and only if those two components - // are already zero. + // color is coerced into a color with maximum third component value and maximum alpha. + // This can only happen here in the spectrum if those two components are already zero. + // + // In the past this coercion was restricted to occur only one time. However, when + // ColorPicker controls are re-used or recycled #00000000 can be set multiple times. + // Each time needs this special logic for usability so now anytime the color is + // changed on the spectrum this logic will run. // // Also note this is NOT currently done for #00FFFFFF (Transparent) but based on // further usability study that case may need to be handled here as well. Right now // Transparent is treated as a normal color value with the alpha intentionally set // to zero so the alpha slider must still be adjusted after the spectrum. - if (!_coercedInitialColor && - IsLoaded) + if (IsLoaded) { bool isAlphaComponentZero = (alpha == 0.0); bool isThirdComponentZero = false; @@ -691,8 +693,6 @@ namespace Avalonia.Controls.Primitives newHsv.H = 360.0; break; } - - _coercedInitialColor = true; } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 96d072260c..d28c0969c4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -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/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/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/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/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 a3f50876e0..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 { @@ -535,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 7bf8d3bb68..56a5999f29 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -84,6 +84,11 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether to automatically scroll to newly selected items. /// + /// + /// This property is of limited use with as it will only scroll + /// to realized items. To scroll to a non-expanded item, you need to ensure that its + /// ancestors are expanded. + /// public bool AutoScrollToSelectedItem { get => GetValue(AutoScrollToSelectedItemProperty); @@ -353,9 +358,13 @@ namespace Avalonia.Controls SelectedItemsAdded(e.NewItems!.Cast().ToArray()); - if (AutoScrollToSelectedItem) + var selectedItem = SelectedItem; + + if (AutoScrollToSelectedItem && + selectedItem is not null && + e.NewItems![0] == selectedItem) { - var container = ContainerFromItem(e.NewItems![0]!); + var container = TreeContainerFromItem(selectedItem); container?.BringIntoView(); } @@ -531,6 +540,12 @@ namespace Avalonia.Controls // The IsSelected property is not set on the container: update the container // selection based on the current selection as understood by this control. MarkContainerSelected(container, SelectedItems.Contains(item)); + + // If the newly realized container is the selected container, scroll to it after layout. + if (AutoScrollToSelectedItem && SelectedItem == item) + { + Dispatcher.UIThread.Post(container.BringIntoView, DispatcherPriority.Loaded); + } } /// 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.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 3adad38ac6..f993d2c957 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -44,6 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels SelectedTab = 0; if (root is TopLevel topLevel) { + _pointerOverRoot = topLevel; _pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x); 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 @@ - - -