diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b5b80fb147..2cdd031693 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using System.Linq; +using System.Net.Http.Headers; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -12,6 +14,11 @@ namespace ControlCatalog.Pages public class ScreenPage : UserControl { private double _leftMost; + private double _topMost; + private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7"); + private IBrush _defaultBrush = Brushes.LightGray; + private IPen _activePen = new Pen(Brushes.Black); + private IPen _defaultPen = new Pen(Brushes.DarkGray); protected override bool BypassFlowDirectionPolicies => true; @@ -34,54 +41,88 @@ namespace ControlCatalog.Pages var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; - var drawBrush = Brushes.Black; - Pen p = new Pen(drawBrush); + var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))); + double maxBottom = 0; - foreach (Screen screen in screens) + for (int i = 0; i + + + diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 0b737dd959..e53ae40cb1 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -41,54 +41,41 @@ namespace Avalonia.Data { case BindingMode.Default: case BindingMode.OneWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - return target.Bind(property, binding.Observable, binding.Priority); + return target.Bind(property, binding.Source, binding.Priority); case BindingMode.TwoWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + { + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); return new TwoWayBindingDisposable( - target.Bind(property, binding.Observable, binding.Priority), - target.GetObservable(property).Subscribe(binding.Subject)); + target.Bind(property, binding.Source, binding.Priority), + target.GetObservable(property).Subscribe(observer)); + } case BindingMode.OneTime: - if (binding.Observable is {} source) - { - // Perf: Avoid allocating closure in the outer scope. - var targetCopy = target; - var propertyCopy = property; - var bindingCopy = binding; - - return source - .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) - .Take(1) - .Subscribe(x => targetCopy.SetValue( - propertyCopy, - BindingNotification.ExtractValue(x), - bindingCopy.Priority)); - } - else - { - target.SetValue(property, binding.Value, binding.Priority); - return Disposable.Empty; - } + { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + + return binding.Source + .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) + .Take(1) + .Subscribe(x => targetCopy.SetValue( + propertyCopy, + BindingNotification.ExtractValue(x), + bindingCopy.Priority)); + } case BindingMode.OneWayToSource: { - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); - // Perf: Avoid allocating closure in the outer scope. - var bindingCopy = binding; - return Observable.CombineLatest( - binding.Observable, + binding.Source, target.GetObservable(property), (_, v) => v) - .Subscribe(x => bindingCopy.Subject.OnNext(x)); + .Subscribe(x => observer.OnNext(x)); } default: diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 536c14dcf9..92fc843394 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -3,7 +3,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { - public class AvaloniaPropertyAccessorNode : SettableNode + internal class AvaloniaPropertyAccessorNode : SettableNode { private IDisposable? _subscription; private readonly bool _enableValidation; diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 55caf8070e..79942cb9ce 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -13,7 +13,7 @@ namespace Avalonia.Data.Core /// that are sent and received. /// [RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] - public class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription + internal class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 4e142fbee9..b333fa9047 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -1,6 +1,6 @@ namespace Avalonia.Data.Core { - public class EmptyExpressionNode : ExpressionNode + internal class EmptyExpressionNode : ExpressionNode { public override string Description => "."; } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 5fb2bb5c13..30fc71cfb4 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Data.Core { - public abstract class ExpressionNode + internal abstract class ExpressionNode { protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 0818b5fa62..ce3549c4ad 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : LightweightObservableBase, IDescription + internal class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 2fad96701d..9ec256225b 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -7,7 +7,7 @@ using Avalonia.Utilities; namespace Avalonia.Data.Core { - public abstract class IndexerNodeBase : SettableNode, + internal abstract class IndexerNodeBase : SettableNode, IWeakEventSubscriber, IWeakEventSubscriber { diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 45837db73d..81b07bfe85 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -3,7 +3,7 @@ using System.Globalization; namespace Avalonia.Data.Core { - public class LogicalNotNode : ExpressionNode, ITransformNode + internal class LogicalNotNode : ExpressionNode, ITransformNode { public override string Description => "!"; diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 34f8e568d4..f111d8917b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -8,7 +8,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Reads a property from a . /// - public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs new file mode 100644 index 0000000000..6d88d55774 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Avalonia.Data.Core.Plugins +{ + /// + /// Holds a registry of plugins used for bindings. + /// + public static class BindingPlugins + { + /// + /// An ordered collection of property accessor plugins that can be used to customize + /// the reading and subscription of property values on a type. + /// + public static IList PropertyAccessors => ExpressionObserver.PropertyAccessors; + + /// + /// An ordered collection of validation checker plugins that can be used to customize + /// the validation of view model and model data. + /// + public static IList DataValidators => ExpressionObserver.DataValidators; + + /// + /// An ordered collection of stream plugins that can be used to customize the behavior + /// of the '^' stream binding operator. + /// + public static IList StreamHandlers => ExpressionObserver.StreamHandlers; + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index bc300386b9..ba5f59ea23 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on that have s. /// - public class DataAnnotationsValidationPlugin : IDataValidationPlugin + internal class DataAnnotationsValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 2bb8da2c74..e60a341309 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - public class ExceptionValidationPlugin : IDataValidationPlugin + internal class ExceptionValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 87a2f67ee8..3384a99333 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on objects that implement . /// - public class IndeiValidationPlugin : IDataValidationPlugin + internal class IndeiValidationPlugin : IDataValidationPlugin { private static readonly WeakEvent ErrorsChangedWeakEvent = WeakEvent.Register( diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 5b19e995cc..7c2caf02b4 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core.Plugins /// Reads a property from a standard C# object that optionally supports the /// interface. /// - public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup = new Dictionary<(Type, string), PropertyInfo?>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 2397ce483d..8170edd653 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -6,7 +6,7 @@ using System.Reflection; namespace Avalonia.Data.Core.Plugins { - public class MethodAccessorPlugin : IPropertyAccessorPlugin + internal class MethodAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup = new Dictionary<(Type, string), MethodInfo?>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index b40628fd35..2b9da0a61a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class ObservableStreamPlugin : IStreamPlugin + internal class ObservableStreamPlugin : IStreamPlugin { private static MethodInfo? s_observableGeneric; private static MethodInfo? s_observableSelect; diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 715f4604cf..42a050778e 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class TaskStreamPlugin : IStreamPlugin + internal class TaskStreamPlugin : IStreamPlugin { /// /// Checks whether this plugin handles the specified value. diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 1b79fed6e7..3898d232ec 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -5,7 +5,7 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class PropertyAccessorNode : SettableNode + internal class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessorPlugin? _customPlugin; diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 9ad9ace814..4980e4487a 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Data.Core { - public abstract class SettableNode : ExpressionNode + internal abstract class SettableNode : ExpressionNode { public bool SetTargetValue(object? value, BindingPriority priority) { diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index ba18a2173b..d3da6414ac 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -6,7 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class StreamNode : ExpressionNode + internal class StreamNode : ExpressionNode { private IStreamPlugin? _customPlugin = null; private IDisposable? _subscription; diff --git a/src/Avalonia.Base/Data/Core/TypeCastNode.cs b/src/Avalonia.Base/Data/Core/TypeCastNode.cs index 3a2ca955fa..655bfbc7a1 100644 --- a/src/Avalonia.Base/Data/Core/TypeCastNode.cs +++ b/src/Avalonia.Base/Data/Core/TypeCastNode.cs @@ -4,7 +4,7 @@ using System.Text; namespace Avalonia.Data.Core { - public class TypeCastNode : ExpressionNode + internal class TypeCastNode : ExpressionNode { public override string Description => $"as {TargetType.FullName}"; diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index a60c1d72ec..00e5c3d8e6 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Reactive; +using ObservableEx = Avalonia.Reactive.Observable; namespace Avalonia.Data { @@ -14,11 +15,23 @@ namespace Avalonia.Data /// public class InstancedBinding { - internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority) + /// + /// Initializes a new instance of the class. + /// + /// The binding source. + /// The binding mode. + /// The priority of the binding. + /// + /// This constructor can be used to create any type of binding and as such requires an + /// as the binding source because this is the only binding + /// source which can be used for all binding modes. If you wish to create an instance with + /// something other than a subject, use one of the static creation methods on this class. + /// + internal InstancedBinding(IObservable source, BindingMode mode, BindingPriority priority) { Mode = mode; Priority = priority; - Value = value; + Source = source ?? throw new ArgumentNullException(nameof(source)); } /// @@ -32,24 +45,12 @@ namespace Avalonia.Data public BindingPriority Priority { get; } /// - /// Gets the value or source of the binding. - /// - public object? Value { get; } - - /// - /// Gets the as an observable. + /// Gets the binding source observable. /// - public IObservable? Observable => Value as IObservable; + public IObservable Source { get; } - /// - /// Gets the as an observer. - /// - public IObserver? Observer => Value as IObserver; - - /// - /// Gets the as an subject. - /// - internal IAvaloniaSubject? Subject => Value as IAvaloniaSubject; + [Obsolete("Use Source property")] + public IObservable Observable => Source; /// /// Creates a new one-time binding with a fixed value. @@ -61,7 +62,7 @@ namespace Avalonia.Data object value, BindingPriority priority = BindingPriority.LocalValue) { - return new InstancedBinding(value, BindingMode.OneTime, priority); + return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority); } /// @@ -106,7 +107,7 @@ namespace Avalonia.Data { _ = observer ?? throw new ArgumentNullException(nameof(observer)); - return new InstancedBinding(observer, BindingMode.OneWayToSource, priority); + return new InstancedBinding((IObservable)observer, BindingMode.OneWayToSource, priority); } /// @@ -135,7 +136,7 @@ namespace Avalonia.Data /// An instance. public InstancedBinding WithPriority(BindingPriority priority) { - return new InstancedBinding(Value, Mode, priority); + return new InstancedBinding(Source, Mode, priority); } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index fc4bc6aa1c..b637c94d88 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -11,64 +13,112 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { - private static readonly IComparer s_ascendingComparer = Comparer.Default; - private static readonly IComparer s_descendingComparer = new ReverseComparer(); - private IGlyphRunImpl? _glyphRunImpl; - private IGlyphTypeface _glyphTypeface; private double _fontRenderingEmSize; private int _biDiLevel; private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private ReadOnlyMemory _characters; - private IReadOnlyList _glyphIndices; - private IReadOnlyList? _glyphAdvances; - private IReadOnlyList? _glyphOffsets; - private IReadOnlyList? _glyphClusters; + private IReadOnlyList _glyphInfos; + private bool _hasOneCharPerCluster; // if true, character index and cluster are similar /// - /// Initializes a new instance of the class by specifying properties of the class. + /// Initializes a new instance of the class by specifying properties of the class. /// /// The glyph typeface. /// The rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. /// The characters. - /// The glyph clusters. + /// The glyph indices. /// The bidi level. public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphIndices, - IReadOnlyList? glyphAdvances = null, - IReadOnlyList? glyphOffsets = null, - IReadOnlyList? glyphClusters = null, + int biDiLevel = 0) + : this(glyphTypeface, fontRenderingEmSize, characters, + CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel) + { + _hasOneCharPerCluster = true; + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The characters. + /// The list of glyphs used. + /// The bidi level. + public GlyphRun( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlyMemory characters, + IReadOnlyList glyphInfos, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + GlyphTypeface = glyphTypeface; _fontRenderingEmSize = fontRenderingEmSize; _characters = characters; - _glyphIndices = glyphIndices; + _glyphInfos = glyphInfos; - _glyphAdvances = glyphAdvances; + _biDiLevel = biDiLevel; + } - _glyphOffsets = glyphOffsets; + private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, + double fontRenderingEmSize, IGlyphTypeface glyphTypeface) + { + var glyphIndexSpan = ListToSpan(glyphIndices); + var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan); - _glyphClusters = glyphClusters; + var glyphInfos = new GlyphInfo[glyphIndexSpan.Length]; + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; - _biDiLevel = biDiLevel; + for (var i = 0; i < glyphIndexSpan.Length; ++i) + { + glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale); + } + + return glyphInfos; + } + + private static ReadOnlySpan ListToSpan(IReadOnlyList list) + { + var count = list.Count; + + if (count == 0) + { + return default; + } + + if (list is ushort[] array) + { + return array.AsSpan(); + } + +#if NET6_0_OR_GREATER + if (list is List concreteList) + { + return CollectionsMarshal.AsSpan(concreteList); + } +#endif + + array = new ushort[count]; + for (var i = 0; i < count; ++i) + { + array[i] = list[i]; + } + + return array.AsSpan(); } /// /// Gets the for the . /// - public IGlyphTypeface GlyphTypeface => _glyphTypeface; + public IGlyphTypeface GlyphTypeface { get; } /// /// Gets or sets the em size used for rendering the . @@ -88,56 +138,17 @@ namespace Avalonia.Media /// /// public GlyphRunMetrics Metrics - { - get - { - _glyphRunMetrics ??= CreateGlyphRunMetrics(); - - return _glyphRunMetrics.Value; - } - } + => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// /// Gets or sets the baseline origin of the. /// public Point BaselineOrigin { - get - { - _baselineOrigin ??= CalculateBaselineOrigin(); - - return _baselineOrigin.Value; - } + get => _baselineOrigin ??= CalculateBaselineOrigin(); set => Set(ref _baselineOrigin, value); } - /// - /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. - /// - public IReadOnlyList GlyphIndices - { - get => _glyphIndices; - set => Set(ref _glyphIndices, value); - } - - /// - /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. - /// - public IReadOnlyList? GlyphAdvances - { - get => _glyphAdvances; - set => Set(ref _glyphAdvances, value); - } - - /// - /// Gets or sets an array of values representing the offsets of the glyphs in the . - /// - public IReadOnlyList? GlyphOffsets - { - get => _glyphOffsets; - set => Set(ref _glyphOffsets, value); - } - /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// @@ -148,12 +159,16 @@ namespace Avalonia.Media } /// - /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// Gets or sets the list of glyphs to use to render this run. /// - public IReadOnlyList? GlyphClusters + public IReadOnlyList GlyphInfos { - get => _glyphClusters; - set => Set(ref _glyphClusters, value); + get => _glyphInfos; + set + { + Set(ref _glyphInfos, value); + _hasOneCharPerCluster = false; + } } /// @@ -179,17 +194,7 @@ namespace Avalonia.Media /// The platform implementation of the . /// public IGlyphRunImpl GlyphRunImpl - { - get - { - if (_glyphRunImpl == null) - { - Initialize(); - } - - return _glyphRunImpl!; - } - } + => _glyphRunImpl ??= CreateGlyphRunImpl(); /// /// Obtains geometry for the glyph run. @@ -221,38 +226,32 @@ namespace Avalonia.Media if (IsLeftToRight) { - if (GlyphClusters != null) + if (characterIndex < Metrics.FirstCluster) { - if (characterIndex < Metrics.FirstCluster) - { - return 0; - } + return 0; + } - if (characterIndex > Metrics.LastCluster) - { - return Metrics.WidthIncludingTrailingWhitespace; - } + if (characterIndex > Metrics.LastCluster) + { + return Metrics.WidthIncludingTrailingWhitespace; } var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null) - { - var currentCluster = GlyphClusters[glyphIndex]; + var currentCluster = _glyphInfos[glyphIndex].GlyphCluster; - //Move to the end of the glyph cluster - if (characterHit.TrailingLength > 0) + //Move to the end of the glyph cluster + if (characterHit.TrailingLength > 0) + { + while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster) { - while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster) - { - glyphIndex++; - } + glyphIndex++; } } for (var i = 0; i < glyphIndex; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return distance; @@ -262,22 +261,19 @@ namespace Avalonia.Media //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null && GlyphClusters.Count > 0) + if (characterIndex > Metrics.LastCluster) { - if (characterIndex > Metrics.LastCluster) - { - return 0; - } + return 0; + } - if (characterIndex <= Metrics.FirstCluster) - { - return Size.Width; - } + if (characterIndex <= Metrics.FirstCluster) + { + return Size.Width; } - for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++) + for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return Size.Width - distance; @@ -322,11 +318,12 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; if (distance > currentX && distance <= currentX + advance) { @@ -340,11 +337,12 @@ namespace Avalonia.Media { currentX = Size.Width; - for (var index = GlyphIndices.Count - 1; index >= 0; index--) + for (var index = _glyphInfos.Count - 1; index >= 0; index--) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; var offsetX = currentX - advance; @@ -424,7 +422,7 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { - if (GlyphClusters == null || GlyphClusters.Count == 0) + if (_hasOneCharPerCluster) { return characterIndex; } @@ -433,7 +431,7 @@ namespace Avalonia.Media { if (IsLeftToRight) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return 0; @@ -446,15 +444,13 @@ namespace Avalonia.Media return 0; } - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } - var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; - - var clusters = GlyphClusters; + var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer; // Find the start of the cluster at the character index. - var start = clusters.BinarySearch(characterIndex, comparer); + var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); // No cluster found. if (start < 0) @@ -463,40 +459,38 @@ namespace Avalonia.Media { characterIndex--; - start = clusters.BinarySearch(characterIndex, comparer); + start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); } if (start < 0) { - goto result; + return 0; } } if (IsLeftToRight) { - while (start > 0 && clusters[start - 1] == clusters[start]) + while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start--; } } else { - while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start]) + while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start++; } } - result: - if (start < 0) { return 0; } - if (start > GlyphIndices.Count - 1) + if (start > _glyphInfos.Count - 1) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return start; @@ -516,14 +510,14 @@ namespace Avalonia.Media var glyphIndex = FindGlyphIndex(index); - if (GlyphClusters == null) + if (_hasOneCharPerCluster) { - width = GetGlyphAdvance(index, out _); + width = _glyphInfos[index].GlyphAdvance; return new CharacterHit(glyphIndex, 1); } - var cluster = GlyphClusters[glyphIndex]; + var cluster = _glyphInfos[glyphIndex].GlyphCluster; var nextCluster = cluster; @@ -531,13 +525,13 @@ namespace Avalonia.Media while (nextCluster == cluster) { - width += GetGlyphAdvance(currentIndex, out _); + width += _glyphInfos[currentIndex].GlyphAdvance; if (IsLeftToRight) { currentIndex++; - if (currentIndex == GlyphClusters.Count) + if (currentIndex == _glyphInfos.Count) { break; } @@ -552,7 +546,7 @@ namespace Avalonia.Media } } - nextCluster = GlyphClusters[currentIndex]; + nextCluster = _glyphInfos[currentIndex].GlyphCluster; } var clusterLength = Math.Max(0, nextCluster - cluster); @@ -565,9 +559,9 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (int i = 1; i < GlyphClusters.Count; i++) + for (int i = 1; i < _glyphInfos.Count; i++) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -583,9 +577,9 @@ namespace Avalonia.Media } else { - for (int i = GlyphClusters.Count - 1; i >= 0; i--) + for (int i = _glyphInfos.Count - 1; i >= 0; i--) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -613,26 +607,6 @@ namespace Avalonia.Media return new CharacterHit(cluster, clusterLength); } - /// - /// Gets a glyph's width. - /// - /// The glyph index. - /// The current cluster. - /// The glyph's width. - private double GetGlyphAdvance(int index, out int cluster) - { - cluster = GlyphClusters != null ? GlyphClusters[index] : index; - - if (GlyphAdvances != null) - { - return GlyphAdvances[index]; - } - - var glyph = GlyphIndices[index]; - - return GlyphTypeface.GetGlyphAdvance(glyph) * Scale; - } - /// /// Calculates the default baseline origin of the . /// @@ -644,20 +618,17 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { - int firstCluster = 0, lastCluster = 0; + int firstCluster, lastCluster; - if (_glyphClusters != null && _glyphClusters.Count > 0) + if (Characters.IsEmpty) { - firstCluster = _glyphClusters[0]; - lastCluster = _glyphClusters[_glyphClusters.Count - 1]; + firstCluster = 0; + lastCluster = 0; } else { - if (!Characters.IsEmpty) - { - firstCluster = 0; - lastCluster = Characters.Length - 1; - } + firstCluster = _glyphInfos[0].GlyphCluster; + lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster; } if (!IsLeftToRight) @@ -671,9 +642,9 @@ namespace Avalonia.Media var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out _); + var advance = _glyphInfos[index].GlyphAdvance; widthIncludingTrailingWhitespace += advance; } @@ -684,14 +655,14 @@ namespace Avalonia.Media { for (var index = 0; index < glyphCount; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } else { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } @@ -710,7 +681,7 @@ namespace Avalonia.Media { if (isReversed) { - return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); + return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } glyphCount = 0; @@ -720,84 +691,59 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = charactersSpan.Length - 1; i >= 0;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); - - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; + var characterIndex = charactersSpan.Length - 1; - i -= count; - glyphCount++; - } - } - else + for (var i = _glyphInfos.Count - 1; i >= 0; i--) { - var characterIndex = charactersSpan.Length - 1; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = GlyphClusters.Count - 1; i >= 0; i--) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex -= characterLength; - characterIndex -= characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + while (i - 1 >= 0) + { + var nextCluster = _glyphInfos[i - 1].GlyphCluster; - while (i - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[i - 1]; + clusterLength++; + i--; - if (currentCluster == nextCluster) + if(characterIndex >= 0) { - clusterLength++; - i--; - - if(characterIndex >= 0) - { - codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); + codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - characterIndex -= characterLength; - } - - continue; + characterIndex -= characterLength; } - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount++; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } return trailingWhitespaceLength; } - private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) { glyphCount = 0; newLineLength = 0; @@ -806,71 +752,46 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = 0; i < charactersSpan.Length;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); + var characterIndex = 0; - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; - - i += count; - glyphCount++; - } - } - else + for (var i = 0; i < _glyphInfos.Count; i++) { - var characterIndex = 0; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = 0; i < GlyphClusters.Count; i++) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex += characterLength; - characterIndex += characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + var j = i; - var j = i; + while (j - 1 >= 0) + { + var nextCluster = _glyphInfos[--j].GlyphCluster; - while (j - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[--j]; - - if (currentCluster == nextCluster) - { - clusterLength++; + clusterLength++; - continue; - } - - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount += clusterLength; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount += clusterLength; } } @@ -890,44 +811,17 @@ namespace Avalonia.Media field = value; } - /// - /// Initializes the . - /// - private void Initialize() + private IGlyphRunImpl CreateGlyphRunImpl() { - if (GlyphIndices == null) - { - throw new InvalidOperationException(); - } - - var glyphCount = GlyphIndices.Count; - - if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount) - { - throw new InvalidOperationException(); - } - - if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount) - { - throw new InvalidOperationException(); - } - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); } public void Dispose() { _glyphRunImpl?.Dispose(); - } - - private class ReverseComparer : IComparer - { - public int Compare(T? x, T? y) - { - return Comparer.Default.Compare(y, x); - } + _glyphRunImpl = null; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs new file mode 100644 index 0000000000..4db55fae6d --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -0,0 +1,268 @@ +using System; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Reorders text runs according to their bidi level. + /// + /// To avoid allocations, this class is designed to be reused. + internal sealed class BidiReorderer + { + [ThreadStatic] private static BidiReorderer? t_instance; + + private ArrayBuilder _runs; + private ArrayBuilder _ranges; + + public static BidiReorderer Instance + => t_instance ??= new(); + + public void BidiReorder(Span textRuns, FlowDirection flowDirection) + { + Debug.Assert(_runs.Length == 0); + Debug.Assert(_ranges.Length == 0); + + if (textRuns.IsEmpty) + { + return; + } + + try + { + _runs.Add(textRuns.Length); + + // Build up the collection of ordered runs. + for (var i = 0; i < textRuns.Length; i++) + { + var textRun = textRuns[i]; + _runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection)); + + if (i > 0) + { + _runs[i - 1].NextRunIndex = i; + } + } + + // Reorder them into visual order. + var firstIndex = LinearReorder(); + + // Now perform a recursive reversal of each run. + // From the highest level found in the text to the lowest odd level on each line, including intermediate levels + // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. + // https://unicode.org/reports/tr9/#L2 + sbyte max = 0; + var min = sbyte.MaxValue; + + for (var i = 0; i < textRuns.Length; i++) + { + var level = GetRunBidiLevel(textRuns[i], flowDirection); + if (level > max) + { + max = level; + } + + if ((level & 1) != 0 && level < min) + { + min = level; + } + } + + if (min > max) + { + min = max; + } + + if (max == 0 || (min == max && (max & 1) == 0)) + { + // Nothing to reverse. + return; + } + + // Now apply the reversal and replace the original contents. + var minLevelToReverse = max; + int currentIndex; + + while (minLevelToReverse >= min) + { + currentIndex = firstIndex; + + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + if (current.Level >= minLevelToReverse && current.Level % 2 != 0) + { + if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) + { + shapedTextCharacters.Reverse(); + } + } + + currentIndex = current.NextRunIndex; + } + + minLevelToReverse--; + } + + var index = 0; + + currentIndex = firstIndex; + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + textRuns[index++] = current.Run; + + currentIndex = current.NextRunIndex; + } + } + finally + { + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges); + } + } + + private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) + { + if (run is ShapedTextRun shapedTextRun) + { + return shapedTextRun.BidiLevel; + } + + var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; + return (sbyte)defaultLevel; + } + + /// + /// Reorders the runs from logical to visual order. + /// + /// + /// The first run index in visual order. + private int LinearReorder() + { + var runIndex = 0; + var rangeIndex = -1; + + while (runIndex >= 0) + { + ref var run = ref _runs[runIndex]; + var nextRunIndex = run.NextRunIndex; + + while (rangeIndex >= 0 + && _ranges[rangeIndex].Level > run.Level + && _ranges[rangeIndex].PreviousRangeIndex >= 0 + && _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level) + { + + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level) + { + // Attach run to the range. + if ((run.Level & 1) != 0) + { + // Odd, range goes to the right of run. + run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex; + _ranges[rangeIndex].LeftRunIndex = runIndex; + } + else + { + // Even, range goes to the left of run. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex; + _ranges[rangeIndex].RightRunIndex = runIndex; + } + + _ranges[rangeIndex].Level = run.Level; + } + else + { + var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex); + _ranges.AddItem(r); + rangeIndex = _ranges.Length - 1; + } + + runIndex = nextRunIndex; + } + + while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0) + { + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + // Terminate. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1; + + return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex; + } + + private int MergeRangeWithPrevious(int index) + { + var previousIndex = _ranges[index].PreviousRangeIndex; + ref var previous = ref _ranges[previousIndex]; + + int leftIndex; + int rightIndex; + + if ((previous.Level & 1) != 0) + { + // Odd, previous goes to the right of range. + leftIndex = index; + rightIndex = previousIndex; + } + else + { + // Even, previous goes to the left of range. + leftIndex = previousIndex; + rightIndex = index; + } + + // Stitch them + ref var left = ref _ranges[leftIndex]; + ref var right = ref _ranges[rightIndex]; + _runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex; + previous.LeftRunIndex = left.LeftRunIndex; + previous.RightRunIndex = right.RightRunIndex; + + return previousIndex; + } + + private struct OrderedBidiRun + { + public OrderedBidiRun(int runIndex, TextRun run, sbyte level) + { + RunIndex = runIndex; + Run = run; + Level = level; + NextRunIndex = -1; + } + + public int RunIndex { get; } + + public sbyte Level { get; } + + public TextRun Run { get; } + + public int NextRunIndex { get; set; } // -1 if none + } + + private struct BidiRange + { + public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex) + { + Level = level; + LeftRunIndex = leftRunIndex; + RightRunIndex = rightRunIndex; + PreviousRangeIndex = previousRangeIndex; + } + + public sbyte Level { get; set; } + + public int LeftRunIndex { get; set; } + + public int RightRunIndex { get; set; } + + public int PreviousRangeIndex { get; } // -1 if none + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs new file mode 100644 index 0000000000..c27903cd55 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal static class FormattingBufferHelper + { + // 1MB, arbitrary, that's 512K characters or 128K object references on x64 + private const long MaxKeptBufferSizeInBytes = 1024 * 1024; + + public static void ClearThenResetIfTooLarge(ref ArrayBuilder arrayBuilder) + { + arrayBuilder.Clear(); + + if (IsBufferTooLarge((uint) arrayBuilder.Capacity)) + { + arrayBuilder = default; + } + } + + public static void ClearThenResetIfTooLarge(List list) + { + list.Clear(); + + if (IsBufferTooLarge((uint) list.Capacity)) + { + list.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(Stack stack) + { + var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count); + + stack.Clear(); + + if (IsBufferTooLarge(approximateCapacity)) + { + stack.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) + where TKey : notnull + { + var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count); + + dictionary.Clear(); + + // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here + if (IsBufferTooLarge>(approximateCapacity)) + { +#if NET6_0_OR_GREATER + dictionary.TrimExcess(); +#else + dictionary = new Dictionary(); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsBufferTooLarge(uint capacity) + => (long) (uint) Unsafe.SizeOf() * capacity > MaxKeptBufferSizeInBytes; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RoundUpToPowerOf2(uint value) + { +#if NET6_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs new file mode 100644 index 0000000000..cb8168e693 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Contains various list pools that are commonly used during text layout. + /// + /// This class provides an instance per thread. + /// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout. + /// + /// + /// + internal sealed class FormattingObjectPool + { + [ThreadStatic] private static FormattingObjectPool? t_instance; + + /// + /// Gets an instance of this class for the current thread. + /// + /// + /// Since this is backed by a thread static field which is slower than a normal static field, + /// prefer passing the instance around when possible instead of calling this property each time. + /// + public static FormattingObjectPool Instance + => t_instance ??= new(); + + public ListPool TextRunLists { get; } = new(); + + public ListPool UnshapedTextRunLists { get; } = new(); + + public ListPool TextLines { get; } = new(); + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + TextRunLists.VerifyAllReturned(); + UnshapedTextRunLists.VerifyAllReturned(); + TextLines.VerifyAllReturned(); + } + + internal sealed class ListPool + { + // we don't need a big number here, these are for temporary usages only which should quickly be returned + private const int MaxSize = 16; + + private readonly RentedList[] _lists = new RentedList[MaxSize]; + private int _size; + private int _pendingReturnCount; + + /// + /// Rents a list. + /// See for the intended usages. + /// + /// A rented list instance that must be returned to the pool. + /// + public RentedList Rent() + { + var list = _size > 0 ? _lists[--_size] : new RentedList(); + + Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!"); + + ++_pendingReturnCount; + return list; + } + + /// + /// Returns a rented list to the pool. + /// + /// + /// On input, the list to return. + /// On output, the reference is set to null to avoid misuse. + /// + public void Return(ref RentedList? rentedList) + { + if (rentedList is null) + { + return; + } + + --_pendingReturnCount; + FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList); + + if (_size < MaxSize) + { + _lists[_size++] = rentedList; + } + + rentedList = null; + } + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + if (_pendingReturnCount > 0) + { + throw new InvalidOperationException( + $"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!"); + } + + if (_pendingReturnCount < 0) + { + throw new InvalidOperationException( + $"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!"); + } + } + } + + /// + /// Represents a list that has been rented through . + /// + /// This class can be used when a temporary list is needed to store some items during text layout. + /// It can also be used as a reusable array builder by calling when done. + /// + /// + /// NEVER use an instance of this type after it's been returned to the pool. + /// AVOID storing an instance of this type into a field or property. + /// AVOID casting an instance of this type to another type. + /// + /// AVOID passing an instance of this type as an argument to a method expecting a standard list, + /// unless you're absolutely sure it won't store it. + /// + /// + /// If you call a method returning an instance of this type, + /// you're now responsible for returning it to the pool. + /// + /// + /// + /// The type of elements in the list. + internal sealed class RentedList : List + { + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs new file mode 100644 index 0000000000..36a07721a6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a single glyph. + /// + public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default) + { + internal static Comparer ClusterAscendingComparer { get; } = + Comparer.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster)); + + internal static Comparer ClusterDescendingComparer { get; } = + Comparer.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster)); + + /// + /// Get the glyph index. + /// + public ushort GlyphIndex { get; } = GlyphIndex; + + /// + /// Get the glyph cluster. + /// + public int GlyphCluster { get; } = GlyphCluster; + + /// + /// Get the glyph advance. + /// + public double GlyphAdvance { get; } = GlyphAdvance; + + /// + /// Get the glyph offset. + /// + public Vector GlyphOffset { get; } = GlyphOffset; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index b518d47a6d..7afb758038 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -48,8 +48,9 @@ namespace Avalonia.Media.TextFormatting var currentPosition = textLine.FirstTextSourceIndex; - foreach (var textRun in lineImpl.TextRuns) + for (var i = 0; i < lineImpl.TextRuns.Count; ++i) { + var textRun = lineImpl.TextRuns[i]; var text = textRun.Text; if (text.IsEmpty) @@ -111,7 +112,7 @@ namespace Avalonia.Media.TextFormatting shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } - glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; + glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; } currentPosition += textRun.Length; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index b05fab08fa..f29bdd4459 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,24 +1,23 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - public sealed class ShapedBuffer : IList, IDisposable + public sealed class ShapedBuffer : IReadOnlyList, IDisposable { - private static readonly IComparer s_clusterComparer = new CompareClusters(); - private bool _bufferRented; - - public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(text, - new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), - glyphTypeface, - fontRenderingEmSize, - bidiLevel) + private GlyphInfo[]? _rentedBuffer; + + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - _bufferRented = true; - Length = bufferLength; + _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); + Text = text; + GlyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); + GlyphTypeface = glyphTypeface; + FontRenderingEmSize = fontRenderingEmSize; + BidiLevel = bidiLevel; } internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -28,12 +27,12 @@ namespace Avalonia.Media.TextFormatting GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; - Length = GlyphInfos.Length; } - internal ArraySlice GlyphInfos { get; } - - public int Length { get; } + internal ArraySlice GlyphInfos { get; private set; } + + public int Length + => GlyphInfos.Length; public IGlyphTypeface GlyphTypeface { get; } @@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting public sbyte BidiLevel { get; } public bool IsLeftToRight => (BidiLevel & 1) == 0; - - public IReadOnlyList GlyphIndices => new GlyphIndexList(GlyphInfos); - - public IReadOnlyList GlyphClusters => new GlyphClusterList(GlyphInfos); - - public IReadOnlyList GlyphAdvances => new GlyphAdvanceList(GlyphInfos); - - public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); public ReadOnlyMemory Text { get; } @@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting } - var comparer = s_clusterComparer; + var comparer = GlyphInfo.ClusterAscendingComparer; - var clusters = GlyphInfos.Span; + var glyphInfos = GlyphInfos.Span; - var searchValue = new GlyphInfo(0, characterIndex); + var searchValue = new GlyphInfo(default, characterIndex, default); - var start = clusters.BinarySearch(searchValue, comparer); + var start = glyphInfos.BinarySearch(searchValue, comparer); if (start < 0) { @@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting { characterIndex--; - searchValue = new GlyphInfo(0, characterIndex); + searchValue = new GlyphInfo(default, characterIndex, default); - start = clusters.BinarySearch(searchValue, comparer); + start = glyphInfos.BinarySearch(searchValue, comparer); } if (start < 0) @@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting } } - while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) + while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster) { start--; } @@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(this, null); } - var firstCluster = GlyphClusters[0]; - var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + var firstCluster = GlyphInfos[0].GlyphCluster; + var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster; @@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(first, second); } - int ICollection.Count => throw new NotImplementedException(); - - bool ICollection.IsReadOnly => true; + int IReadOnlyCollection.Count => GlyphInfos.Length; public GlyphInfo this[int index] { @@ -144,177 +133,18 @@ namespace Avalonia.Media.TextFormatting set => GlyphInfos[index] = value; } - int IList.IndexOf(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.Insert(int index, GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.RemoveAt(int index) - { - throw new NotImplementedException(); - } - - void ICollection.Add(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.Clear() - { - throw new NotImplementedException(); - } - - bool ICollection.Contains(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.CopyTo(GlyphInfo[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection.Remove(GlyphInfo item) - { - throw new NotImplementedException(); - } public IEnumerator GetEnumerator() => GlyphInfos.GetEnumerator(); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - - private class CompareClusters : IComparer - { - private static readonly Comparer s_intClusterComparer = Comparer.Default; - - public int Compare(GlyphInfo x, GlyphInfo y) - { - return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster); - } - } - - private readonly struct GlyphAdvanceList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphAdvanceList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public double this[int index] => _glyphInfos[index].GlyphAdvance; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphIndexList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphIndexList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public ushort this[int index] => _glyphInfos[index].GlyphIndex; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphClusterList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphClusterList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public int this[int index] => _glyphInfos[index].GlyphCluster; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphOffsetList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphOffsetList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public Vector this[int index] => _glyphInfos[index].GlyphOffset; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Dispose() { - GC.SuppressFinalize(this); - if (_bufferRented) + if (_rentedBuffer is not null) { - GlyphInfos.ReturnRent(); + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + GlyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array } } - - ~ShapedBuffer() - { - if (_bufferRented) - { - GlyphInfos.ReturnRent(); - } - } - } - - public readonly record struct GlyphInfo - { - public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) - { - GlyphIndex = glyphIndex; - GlyphAdvance = glyphAdvance; - GlyphCluster = glyphCluster; - GlyphOffset = glyphOffset; - } - - /// - /// Get the glyph index. - /// - public ushort GlyphIndex { get; } - - /// - /// Get the glyph cluster. - /// - public int GlyphCluster { get; } - - /// - /// Get the glyph advance. - /// - public double GlyphAdvance { get; } - - /// - /// Get the glyph offset. - /// - public Vector GlyphOffset { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 583f2e49f1..d444a58297 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting public override Size Size => GlyphRun.Size; - public GlyphRun GlyphRun - { - get - { - if(_glyphRun is null) - { - _glyphRun = CreateGlyphRun(); - } - - return _glyphRun; - } - } + public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { - if (GlyphRun.GlyphIndices.Count == 0) + if (GlyphRun.GlyphInfos.Count == 0) { return; } @@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < ShapedBuffer.Length; i++) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (currentWidth + advance > availableWidth) { @@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (width + advance > availableWidth) { @@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, Text, - ShapedBuffer.GlyphIndices, - ShapedBuffer.GlyphAdvances, - ShapedBuffer.GlyphOffsets, - ShapedBuffer.GlyphClusters, + ShapedBuffer, BidiLevel); } diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 53021c4656..c1ac57ce46 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -26,5 +26,16 @@ /// The second part. /// public T? Second { get; } + + /// + /// Deconstructs the split results into its components. + /// + /// On return, contains the first part. + /// On return, contains the second part. + public void Deconstruct(out T first, out T? second) + { + first = First; + second = Second; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 2525f0dbf9..c1f3816e54 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, + ref TextRunProperties? previousProperties, RentedList results) { - var shapeableCharacters = new List(2); var properties = Properties; while (!text.IsEmpty) { var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); - shapeableCharacters.Add(shapeableRun); + results.Add(shapeableRun); text = text.Slice(shapeableRun.Length); previousProperties = shapeableRun.Properties; } - - return shapeableCharacters; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 01804e1ce3..72882df0b5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract TextRun[]? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 528cd45581..e6743f5533 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,15 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -22,7 +23,7 @@ namespace Avalonia.Media.TextFormatting if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol - return new List(0); + return Array.Empty(); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -70,18 +71,7 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= shapedRun.Size.Width; @@ -94,18 +84,7 @@ namespace Avalonia.Media.TextFormatting //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= drawableRun.Size.Width; @@ -121,5 +100,27 @@ namespace Avalonia.Media.TextFormatting return null; } + + private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, + TextRun shapedSymbol) + { + if (collapsedLength <= 0) + { + return new[] { shapedSymbol }; + } + + var objectPool = FormattingObjectPool.Instance; + + var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); + + var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + + return collapsedRuns; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8afecb09e2..bf9f6f77f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,15 +1,22 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { - internal class TextFormatterImpl : TextFormatter + internal sealed class TextFormatterImpl : TextFormatter { private static readonly char[] s_empty = { ' ' }; + private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; + + [ThreadStatic] private static BidiData? t_bidiData; + [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; /// public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, @@ -18,20 +25,25 @@ namespace Avalonia.Media.TextFormatting var textWrapping = paragraphProperties.TextWrapping; FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - IReadOnlyList textRuns; + IReadOnlyList? textRuns; + var objectPool = FormattingObjectPool.Instance; - var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, + var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); - if (previousLineBreak?.RemainingRuns != null) + RentedList? shapedTextRuns; + + if (previousLineBreak?.RemainingRuns is { } remainingRuns) { resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = previousLineBreak.RemainingRuns; + textRuns = remainingRuns; nextLineBreak = previousLineBreak; + shapedTextRuns = null; } else { - textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); + textRuns = shapedTextRuns; if (nextLineBreak == null && textEndOfLine != null) { @@ -44,25 +56,32 @@ namespace Avalonia.Media.TextFormatting switch (textWrapping) { case TextWrapping.NoWrap: - { - textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); + { + // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); - textLine.FinalizeLine(); + textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); - break; - } + textLine.FinalizeLine(); + + break; + } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: - { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, - resolvedFlowDirection, nextLineBreak); - break; - } + { + textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + break; + } default: throw new ArgumentOutOfRangeException(nameof(textWrapping)); } + objectPool.TextRunLists.Return(ref shapedTextRuns); + objectPool.TextRunLists.Return(ref fetchedRuns); + return textLine; } @@ -71,9 +90,12 @@ namespace Avalonia.Media.TextFormatting /// /// The text run's. /// The length to split at. + /// A pool used to get reusable formatting objects. /// The split text runs. - internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length, + FormattingObjectPool objectPool) { + var first = objectPool.TextRunLists.Rent(); var currentLength = 0; for (var i = 0; i < textRuns.Count; i++) @@ -89,8 +111,6 @@ namespace Avalonia.Media.TextFormatting var firstCount = currentRun.Length >= 1 ? i + 1 : i; - var first = new List(firstCount); - if (firstCount > 1) { for (var j = 0; j < i; j++) @@ -103,7 +123,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.Length == length) { - var second = secondCount > 0 ? new List(secondCount) : null; + var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; if (second != null) { @@ -117,13 +137,13 @@ namespace Avalonia.Media.TextFormatting first.Add(currentRun); - return new SplitResult>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(secondCount); + var second = objectPool.TextRunLists.Rent(); if (currentRun is ShapedTextRun shapedTextCharacters) { @@ -139,11 +159,16 @@ namespace Avalonia.Media.TextFormatting second.Add(textRuns[i + j]); } - return new SplitResult>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + for (var i = 0; i < textRuns.Count; i++) + { + first.Add(textRuns[i]); + } + + return new SplitResult>(first, null); } /// @@ -152,40 +177,58 @@ namespace Avalonia.Media.TextFormatting /// The text runs to shape. /// The default paragraph properties. /// The resolved flow direction. + /// A pool used to get reusable formatting objects. /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, + private static RentedList ShapeTextRuns(IReadOnlyList textRuns, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var shapedRuns = new List(); - using var biDiData = new BidiData((sbyte)flowDirection); + var shapedRuns = objectPool.TextRunLists.Rent(); - foreach (var textRun in textRuns) + if (textRuns.Count == 0) { - if (textRun.Text.IsEmpty) - { - biDiData.Append(new char[textRun.Length]); - } - else - { - biDiData.Append(textRun.Text.Span); - } + resolvedFlowDirection = flowDirection; + return shapedRuns; } - using var biDi = new BidiAlgorithm(); + var bidiData = t_bidiData ??= new(); + bidiData.Reset(); + bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; + + for (var i = 0; i < textRuns.Count; ++i) + { + var textRun = textRuns[i]; + + ReadOnlySpan text; + if (!textRun.Text.IsEmpty) + text = textRun.Text.Span; + else if (textRun.Length == TextRun.DefaultTextSourceLength) + text = s_defaultText; + else + text = new char[textRun.Length]; + + bidiData.Append(text); + } - biDi.Process(biDiData); + var bidiAlgorithm = t_bidiAlgorithm ??= new(); + bidiAlgorithm.Process(bidiData); - var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes); + var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes); resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; - var processedRuns = new List(textRuns.Count); + var processedRuns = objectPool.TextRunLists.Rent(); + + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); - CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); + bidiData.Reset(); + bidiAlgorithm.Reset(); + + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); for (var index = 0; index < processedRuns.Count; index++) { @@ -194,8 +237,9 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case UnshapedTextRun shapeableRun: - { - var groupedRuns = new List(2) { shapeableRun }; + { + groupedRuns.Clear(); + groupedRuns.Add(shapeableRun); var text = shapeableRun.Text; while (index + 1 < processedRuns.Count) @@ -224,7 +268,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); + ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); break; } @@ -237,6 +281,9 @@ namespace Avalonia.Media.TextFormatting } } + objectPool.TextRunLists.Return(ref processedRuns); + objectPool.UnshapedTextRunLists.Return(ref groupedRuns); + return shapedRuns; } @@ -303,17 +350,14 @@ namespace Avalonia.Media.TextFormatting } } - private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) && x.Typeface == y.Typeface && x.BaselineAlignment == y.BaselineAlignment; - private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, ReadOnlyMemory text, TextShaperOptions options) + private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, + TextShaperOptions options, RentedList results) { - var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) @@ -322,12 +366,10 @@ namespace Avalonia.Media.TextFormatting var splitResult = shapedBuffer.Split(currentRun.Length); - shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); + results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); shapedBuffer = splitResult.Second!; } - - return shapedRuns; } /// @@ -335,10 +377,10 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. - /// + /// A list that will be filled with the processed runs. /// - private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, - List processedRuns) + private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, + RentedList processedRuns) { if (levels.Length == 0) { @@ -385,7 +427,8 @@ namespace Avalonia.Media.TextFormatting if (j == runTextSpan.Length) { - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runLevel = levels[levelIndex]; @@ -398,7 +441,8 @@ namespace Avalonia.Media.TextFormatting } // End of this run - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runText = runText.Slice(j); runTextSpan = runText.Span; @@ -415,7 +459,7 @@ namespace Avalonia.Media.TextFormatting return; } - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns); } /// @@ -423,19 +467,20 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. - /// - /// + /// A pool used to get reusable formatting objects. + /// On return, the end of line, if any. + /// On return, the processed text source length. /// /// The formatted text runs. /// - private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, - out TextEndOfLine? endOfLine, out int textSourceLength) + private static RentedList FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, + FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength) { textSourceLength = 0; endOfLine = null; - var textRuns = new List(); + var textRuns = objectPool.TextRunLists.Rent(); var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); @@ -529,8 +574,10 @@ namespace Avalonia.Media.TextFormatting measuredLength = 0; var currentWidth = 0.0; - foreach (var currentRun in textRuns) + for (var i = 0; i < textRuns.Count; ++i) { + var currentRun = textRuns[i]; + switch (currentRun) { case ShapedTextRun shapedTextCharacters: @@ -540,15 +587,15 @@ namespace Avalonia.Media.TextFormatting var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; - for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) + for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) { - var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; + var glyphInfo = shapedTextCharacters.ShapedBuffer[j]; if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { measuredLength += Math.Max(0, lastCluster - firstCluster); - goto found; + return measuredLength != 0; } lastCluster = glyphInfo.GlyphCluster; @@ -565,7 +612,7 @@ namespace Avalonia.Media.TextFormatting { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { - goto found; + return measuredLength != 0; } measuredLength += currentRun.Length; @@ -582,8 +629,6 @@ namespace Avalonia.Media.TextFormatting } } - found: - return measuredLength != 0; } @@ -591,20 +636,23 @@ namespace Avalonia.Media.TextFormatting /// Creates an empty text line. /// /// The empty text line. - public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) + public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.Typeface.GlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); - var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; + var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); - var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; + var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); + var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); + line.FinalizeLine(); + return line; } /// @@ -616,14 +664,15 @@ namespace Avalonia.Media.TextFormatting /// The text paragraph properties. /// /// The current line break if the line was explicitly broken. + /// A pool used to get reusable formatting objects. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak) + TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -749,12 +798,10 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitTextRuns(textRuns, measuredLength); - - var remainingCharacters = splitResult.Second; + var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); - var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : + var lineBreak = postSplitRuns?.Count > 0 ? + new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) @@ -762,11 +809,16 @@ namespace Avalonia.Media.TextFormatting lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } - var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, + var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); - return textLine.FinalizeLine(); + textLine.FinalizeLine(); + + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + + return textLine; } private struct TextRunEnumerator diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 468623b356..7a74dc89ae 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; + private readonly TextLine[] _textLines; private int _textSourceLength; @@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text lines. /// - public IReadOnlyList TextLines { get; private set; } + public IReadOnlyList TextLines + => _textLines; /// /// Gets the bounds of the layout. @@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting /// The origin. public void Draw(DrawingContext context, Point origin) { - if (!TextLines.Any()) + if (_textLines.Length == 0) { return; } var (currentX, currentY) = origin; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); @@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting /// public Rect HitTestTextPosition(int textPosition) { - if (TextLines.Count == 0) + if (_textLines.Length == 0) { return new Rect(); } @@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting var currentY = 0.0; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { var end = textLine.FirstTextSourceIndex + textLine.Length; @@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting return Array.Empty(); } - var result = new List(TextLines.Count); + var result = new List(_textLines.Length); var currentY = 0d; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { //Current line isn't covered. if (textLine.FirstTextSourceIndex + textLine.Length < start) @@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting { var currentY = 0d; - var lineIndex = 0; TextLine? currentLine = null; CharacterHit characterHit; - for (; lineIndex < TextLines.Count; lineIndex++) + for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++) { - currentLine = TextLines[lineIndex]; + currentLine = _textLines[lineIndex]; if (currentY + currentLine.Height > point.Y) { @@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting if (charIndex > _textSourceLength) { - return TextLines.Count - 1; + return _textLines.Length - 1; } - for (var index = 0; index < TextLines.Count; index++) + for (var index = 0; index < _textLines.Length; index++) { - var textLine = TextLines[index]; + var textLine = _textLines[index]; if (textLine.FirstTextSourceIndex + textLine.Length < charIndex) { @@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting } } - return TextLines.Count - 1; + return _textLines.Length - 1; } private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) @@ -424,18 +424,21 @@ namespace Avalonia.Media.TextFormatting height += textLine.Height; } - private IReadOnlyList CreateTextLines() + private TextLine[] CreateTextLines() { + var objectPool = FormattingObjectPool.Instance; + if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, + FormattingObjectPool.Instance); Bounds = new Rect(0, 0, 0, textLine.Height); - return new List { textLine }; + return new TextLine[] { textLine }; } - var textLines = new List(); + var textLines = objectPool.TextLines.Rent(); double left = double.PositiveInfinity, width = 0.0, height = 0.0; @@ -443,16 +446,19 @@ namespace Avalonia.Media.TextFormatting TextLine? previousLine = null; + var textFormatter = TextFormatter.Current; + while (true) { - var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, - _paragraphProperties, previousLine?.TextLineBreak); + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, + previousLine?.TextLineBreak); - if(textLine == null || textLine.Length == 0) + if (textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { - var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties); + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, + _paragraphProperties, objectPool); textLines.Add(emptyTextLine); @@ -494,7 +500,7 @@ namespace Avalonia.Media.TextFormatting //Fulfill max lines constraint if (MaxLines > 0 && textLines.Count >= MaxLines) { - if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null) + if(textLine.TextLineBreak?.RemainingRuns is not null) { textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); } @@ -511,7 +517,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool); textLines.Add(textLine); @@ -524,8 +530,9 @@ namespace Avalonia.Media.TextFormatting { var whitespaceWidth = 0d; - foreach (var line in textLines) + for (var i = 0; i < textLines.Count; i++) { + var line = textLines[i]; var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; if (lineWhitespaceWidth > whitespaceWidth) @@ -549,7 +556,12 @@ namespace Avalonia.Media.TextFormatting } } - return textLines; + var result = textLines.ToArray(); + + objectPool.TextLines.Return(ref textLines); + objectPool.VerifyAllReturned(); + + return result; } /// @@ -569,7 +581,7 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { - foreach (var line in TextLines) + foreach (var line in _textLines) { line.Dispose(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index e30a0fe9f4..0d777ad043 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,5 +1,7 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Collections.Generic; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting if (Width < shapedSymbol.GlyphRun.Size.Width) { - return new List(0); + return Array.Empty(); } // Overview of ellipsis structure @@ -75,55 +78,66 @@ namespace Avalonia.Media.TextFormatting { shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - var collapsedRuns = new List(textRuns.Count); - if (measuredLength > 0) { - IReadOnlyList? preSplitRuns = null; - IReadOnlyList? postSplitRuns; + var objectPool = FormattingObjectPool.Instance; - if (_prefixLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, - Math.Min(_prefixLength, measuredLength)); + var collapsedRuns = objectPool.TextRunLists.Rent(); - collapsedRuns.AddRange(splitResult.First); + RentedList? rentedPreSplitRuns = null; + RentedList? rentedPostSplitRuns = null; + TextRun[]? results; - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; - } - else + try { - postSplitRuns = textRuns; - } + IReadOnlyList? effectivePostSplitRuns; - collapsedRuns.Add(shapedSymbol); + if (_prefixLength > 0) + { + (rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns( + textRuns, Math.Min(_prefixLength, measuredLength), objectPool); - if (measuredLength <= _prefixLength || postSplitRuns is null) - { - return collapsedRuns; - } + effectivePostSplitRuns = rentedPostSplitRuns; + + foreach (var preSplitRun in rentedPreSplitRuns) + { + collapsedRuns.Add(preSplitRun); + } + } + else + { + effectivePostSplitRuns = textRuns; + } - var availableSuffixWidth = availableWidth; + collapsedRuns.Add(shapedSymbol); - if (preSplitRuns is not null) - { - foreach (var run in preSplitRuns) + if (measuredLength <= _prefixLength || effectivePostSplitRuns is null) { - if (run is DrawableTextRun drawableTextRun) + results = collapsedRuns.ToArray(); + objectPool.TextRunLists.Return(ref collapsedRuns); + return results; + } + + var availableSuffixWidth = availableWidth; + + if (rentedPreSplitRuns is not null) + { + foreach (var run in rentedPreSplitRuns) { - availableSuffixWidth -= drawableTextRun.Size.Width; + if (run is DrawableTextRun drawableTextRun) + { + availableSuffixWidth -= drawableTextRun.Size.Width; + } } } - } - - for (var i = postSplitRuns.Count - 1; i >= 0; i--) - { - var run = postSplitRuns[i]; - switch (run) + for (var i = effectivePostSplitRuns.Count - 1; i >= 0; i--) { - case ShapedTextRun endShapedRun: + var run = effectivePostSplitRuns[i]; + + switch (run) + { + case ShapedTextRun endShapedRun: { if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, out var suffixCount, out var suffixWidth)) @@ -141,15 +155,21 @@ namespace Avalonia.Media.TextFormatting break; } + } } } - } - else - { - collapsedRuns.Add(shapedSymbol); + finally + { + objectPool.TextRunLists.Return(ref rentedPreSplitRuns); + objectPool.TextRunLists.Return(ref rentedPostSplitRuns); + } + + results = collapsedRuns.ToArray(); + objectPool.TextRunLists.Return(ref collapsedRuns); + return results; } - return collapsedRuns; + return new TextRun[] { shapedSymbol }; } availableWidth -= shapedRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ab9686a34a..245104f8fe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,19 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - internal class TextLineImpl : TextLine + internal sealed class TextLineImpl : TextLine { - private IReadOnlyList _textRuns; + private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { @@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, _resolvedFlowDirection, TextLineBreak, true); - if (collapsedRuns.Count > 0) + if (collapsedRuns.Length > 0) { collapsedLine.FinalizeLine(); } @@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetCharacterHitFromDistance(double distance) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Count - 1]; + var lastRun = _textRuns[_textRuns.Length - 1]; var size = 0.0; @@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i < _textRuns.Length; i++) { var currentRun = _textRuns[i]; @@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = i; currentPosition += currentRun.Length; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1) { var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; @@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if (j > _textRuns.Count - 1) + if (j > _textRuns.Length - 1) { break; } @@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting if (currentRun is DrawableTextRun drawableTextRun) { - if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) + if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance) { currentDistance += drawableTextRun.Size.Width; @@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting if (flowDirection == FlowDirection.LeftToRight) { - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { var currentRun = _textRuns[index]; @@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftWidth = shapedRun.Size.Width; - while (i + 1 <= _textRuns.Count - 1) + while (i + 1 <= _textRuns.Length - 1) { var nextRun = _textRuns[i + 1]; @@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting { currentDistance += WidthIncludingTrailingWhitespace; - for (var index = _textRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { var currentRun = _textRuns[index]; @@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -568,7 +567,7 @@ namespace Avalonia.Media.TextFormatting { var characterIndex = firstTextSourceIndex + textLength; - var result = new List(TextRuns.Count); + var result = new List(_textRuns.Length); var lastDirection = FlowDirection.LeftToRight; var currentDirection = lastDirection; @@ -581,9 +580,9 @@ namespace Avalonia.Media.TextFormatting TextRunBounds lastRunBounds = default; - for (var index = 0; index < TextRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { - if (TextRuns[index] is not DrawableTextRun currentRun) + if (_textRuns[index] is not DrawableTextRun currentRun) { continue; } @@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = index; var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) { if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { @@ -669,12 +668,12 @@ namespace Avalonia.Media.TextFormatting for (int i = rightToLeftIndex - 1; i >= index; i--) { - if (TextRuns[i] is not ShapedTextRun) + if (_textRuns[i] is not ShapedTextRun shapedRun) { continue; } - currentShapedRun = (ShapedTextRun)TextRuns[i]; + currentShapedRun = shapedRun; currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); @@ -784,7 +783,7 @@ namespace Avalonia.Media.TextFormatting { var characterIndex = firstTextSourceIndex + textLength; - var result = new List(TextRuns.Count); + var result = new List(_textRuns.Length); var lastDirection = FlowDirection.LeftToRight; var currentDirection = lastDirection; @@ -795,9 +794,9 @@ namespace Avalonia.Media.TextFormatting double currentWidth = 0; var currentRect = default(Rect); - for (var index = TextRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { - if (TextRuns[index] is not DrawableTextRun currentRun) + if (_textRuns[index] is not DrawableTextRun currentRun) { continue; } @@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting public override void Dispose() { - for (int i = 0; i < _textRuns.Count; i++) + for (int i = 0; i < _textRuns.Length; i++) { if (_textRuns[i] is ShapedTextRun shapedTextRun) { @@ -990,186 +989,11 @@ namespace Avalonia.Media.TextFormatting } } - public TextLineImpl FinalizeLine() + public void FinalizeLine() { _textLineMetrics = CreateLineMetrics(); - BidiReorder(); - - return this; - } - - private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) - { - if (run is ShapedTextRun shapedTextCharacters) - { - return shapedTextCharacters.BidiLevel; - } - - var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; - - return (sbyte)defaultLevel; - } - - private void BidiReorder() - { - if (_textRuns.Count == 0) - { - return; - } - - // Build up the collection of ordered runs. - var run = _textRuns[0]; - - OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection)); - - var current = orderedRun; - - for (var i = 1; i < _textRuns.Count; i++) - { - run = _textRuns[i]; - - current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection)); - - current = current.Next; - } - - // Reorder them into visual order. - orderedRun = LinearReOrder(orderedRun); - - // Now perform a recursive reversal of each run. - // From the highest level found in the text to the lowest odd level on each line, including intermediate levels - // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. - // https://unicode.org/reports/tr9/#L2 - sbyte max = 0; - var min = sbyte.MaxValue; - - for (var i = 0; i < _textRuns.Count; i++) - { - var currentRun = _textRuns[i]; - - var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection); - - if (level > max) - { - max = level; - } - - if ((level & 1) != 0 && level < min) - { - min = level; - } - } - - if (min > max) - { - min = max; - } - - if (max == 0 || (min == max && (max & 1) == 0)) - { - // Nothing to reverse. - return; - } - - // Now apply the reversal and replace the original contents. - var minLevelToReverse = max; - - while (minLevelToReverse >= min) - { - current = orderedRun; - - while (current != null) - { - if (current.Level >= minLevelToReverse && current.Level % 2 != 0) - { - if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) - { - shapedTextCharacters.Reverse(); - } - } - - current = current.Next; - } - - minLevelToReverse--; - } - - var textRuns = new List(_textRuns.Count); - - current = orderedRun; - - while (current != null) - { - textRuns.Add(current.Run); - - current = current.Next; - } - - _textRuns = textRuns; - } - - /// - /// Reorders a series of runs from logical to visual order, returning the left most run. - /// - /// - /// The ordered bidi run. - /// The . - private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run) - { - BidiRange? range = null; - - while (run != null) - { - var next = run.Next; - - while (range != null && range.Level > run.Level - && range.Previous != null && range.Previous.Level >= run.Level) - { - range = BidiRange.MergeWithPrevious(range); - } - - if (range != null && range.Level >= run.Level) - { - // Attach run to the range. - if ((run.Level & 1) != 0) - { - // Odd, range goes to the right of run. - run.Next = range.Left; - range.Left = run; - } - else - { - // Even, range goes to the left of run. - range.Right!.Next = run; - range.Right = run; - } - - range.Level = run.Level; - } - else - { - var r = new BidiRange(); - - r.Left = r.Right = run; - r.Level = run.Level; - r.Previous = range; - - range = r; - } - - run = next; - } - - while (range?.Previous != null) - { - range = BidiRange.MergeWithPrevious(range); - } - - // Terminate. - range!.Right!.Next = null; - - return range.Left!; + BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); } /// @@ -1197,7 +1021,7 @@ namespace Avalonia.Media.TextFormatting var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1346,7 +1170,7 @@ namespace Avalonia.Media.TextFormatting textPosition = FirstTextSourceIndex; TextRun? previousRun = null; - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1395,7 +1219,7 @@ namespace Avalonia.Media.TextFormatting } } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1411,7 +1235,7 @@ namespace Avalonia.Media.TextFormatting return runIndex; } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1448,14 +1272,14 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; - var lastRunIndex = _textRuns.Count - 1; + var lastRunIndex = _textRuns.Length - 1; if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) { lastRunIndex--; } - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) { @@ -1620,59 +1444,5 @@ namespace Avalonia.Media.TextFormatting return 0; } } - - private sealed class OrderedBidiRun - { - public OrderedBidiRun(TextRun run, sbyte level) - { - Run = run; - Level = level; - } - - public sbyte Level { get; } - - public TextRun Run { get; } - - public OrderedBidiRun? Next { get; set; } - } - - private sealed class BidiRange - { - public int Level { get; set; } - - public OrderedBidiRun? Left { get; set; } - - public OrderedBidiRun? Right { get; set; } - - public BidiRange? Previous { get; set; } - - public static BidiRange MergeWithPrevious(BidiRange range) - { - var previous = range.Previous; - - BidiRange left; - BidiRange right; - - if ((previous!.Level & 1) != 0) - { - // Odd, previous goes to the right of range. - left = range; - right = previous; - } - else - { - // Even, previous goes to the left of range. - left = previous; - right = range; - } - - // Stitch them - left.Right!.Next = right.Left; - previous.Left = left.Left; - previous.Right = right.Right; - - return previous; - } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 86b701cb4b..7bad99f33f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -93,6 +93,9 @@ namespace Avalonia.Media.TextFormatting internal TextRunProperties WithTypeface(Typeface typeface) { + if (this is GenericTextRunProperties other && other.Typeface == typeface) + return this; + return new GenericTextRunProperties(typeface, FontRenderingEmSize, TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index deecbbe476..ccae99cc75 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// A collapsing properties to collapse whole line toward the end @@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c291e1dfb9..c622c76a60 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using Avalonia.Utilities; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// a collapsing properties to collapse whole line toward the end @@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 100d381afe..3a81784152 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Threading; -using Avalonia.Collections.Pooled; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -28,8 +26,13 @@ namespace Avalonia.Media.TextFormatting.Unicode /// as much as possible. /// /// - internal struct BidiAlgorithm : IDisposable + internal sealed class BidiAlgorithm { + /// + /// Whether the state is clean and can be reused without a reset. + /// + private bool _hasCleanState = true; + /// /// The original BiDiClass classes as provided by the caller /// @@ -67,7 +70,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The forward mapping maps the start index to the end index. /// The reverse mapping maps the end index to the start index. /// - private BidiDictionary? _isolatePairs; + private readonly BidiDictionary _isolatePairs = new(); /// /// The working BiDi classes @@ -98,7 +101,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The status stack used during resolution of explicit /// embedding and isolating runs /// - private readonly Stack _statusStack = new Stack(); + private readonly Stack _statusStack = new(); /// /// Mapping used to virtually remove characters for rule X9 @@ -108,7 +111,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Re-usable list of level runs /// - private readonly List _levelRuns = new List(); + private readonly List _levelRuns = new(); /// /// Mapping for the current isolating sequence, built @@ -119,7 +122,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private Stack? _pendingIsolateOpenings; + private readonly Stack _pendingIsolateOpenings = new(); /// /// The level of the isolating run currently being processed @@ -175,12 +178,12 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Reusable list of pending opening brackets used by the /// LocatePairedBrackets method /// - private readonly List _pendingOpeningBrackets = new List(); + private readonly List _pendingOpeningBrackets = new(); /// /// Resolved list of paired brackets /// - private readonly List _pairedBrackets = new List(); + private readonly List _pairedBrackets = new(); /// /// Initializes a new instance of the class. @@ -228,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs?.Clear(); - _workingClassesBuffer.Clear(); - _levelRuns.Clear(); - _resolvedLevelsBuffer.Clear(); + Reset(); if (types.IsEmpty) { return; } + _hasCleanState = false; + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); @@ -324,7 +326,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // Skip isolate pairs // (Because we're working with a slice, we need to adjust the indices // we're using for the isolatePairs map) - if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true) + if (_isolatePairs.TryGetValue(data.Start + i, out i)) { i -= data.Start; } @@ -359,7 +361,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _hasIsolates = false; // BD9... - _pendingIsolateOpenings?.Clear(); + _pendingIsolateOpenings.Clear(); for (var i = 0; i < _originalClasses.Length; i++) { @@ -371,16 +373,14 @@ namespace Avalonia.Media.TextFormatting.Unicode case BidiClass.RightToLeftIsolate: case BidiClass.FirstStrongIsolate: { - _pendingIsolateOpenings ??= new Stack(); _pendingIsolateOpenings.Push(i); _hasIsolates = true; break; } case BidiClass.PopDirectionalIsolate: { - if (_pendingIsolateOpenings?.Count > 0) + if (_pendingIsolateOpenings.Count > 0) { - _isolatePairs ??= new BidiDictionary(); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); } @@ -501,7 +501,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (resolvedIsolate == BidiClass.FirstStrongIsolate) { - if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) + if (!_isolatePairs.TryGetValue(i, out var endOfIsolate)) { endOfIsolate = _originalClasses.Length; } @@ -832,7 +832,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastType = _originalClasses[lastCharacterIndex]; if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && - _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true) + _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); @@ -855,13 +855,14 @@ namespace Avalonia.Media.TextFormatting.Unicode private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel) { // Create mappings onto the underlying data - _runResolvedClasses = new MappedArraySlice(_workingClasses, _isolatedRunMapping.AsSlice()); - _runOriginalClasses = new MappedArraySlice(_originalClasses, _isolatedRunMapping.AsSlice()); - _runLevels = new MappedArraySlice(_resolvedLevels, _isolatedRunMapping.AsSlice()); + var isolatedRunMapping = _isolatedRunMapping.AsSlice(); + _runResolvedClasses = new MappedArraySlice(_workingClasses, isolatedRunMapping); + _runOriginalClasses = new MappedArraySlice(_originalClasses, isolatedRunMapping); + _runLevels = new MappedArraySlice(_resolvedLevels, isolatedRunMapping); if (_hasBrackets) { - _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, _isolatedRunMapping.AsSlice()); - _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, _isolatedRunMapping.AsSlice()); + _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, isolatedRunMapping); + _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, isolatedRunMapping); } _runLevel = runLevel; @@ -1642,6 +1643,47 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + /// + /// Resets the bidi algorithm to a clean state. + /// + public void Reset() + { + if (_hasCleanState) + { + return; + } + + _originalClasses = default; + _pairedBracketTypes = default; + _pairedBracketValues = default; + _hasBrackets = default; + _hasEmbeddings = default; + _hasIsolates = default; + _isolatePairs.ClearThenResetIfTooLarge(); + _workingClasses = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer); + _resolvedLevels = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer); + _paragraphEmbeddingLevel = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map); + FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings); + _runLevel = default; + _runDirection = default; + _runLength = default; + _runResolvedClasses = default; + _runOriginalClasses = default; + _runLevels = default; + _runBiDiPairedBracketTypes = default; + _runPairedBracketValues = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets); + + _hasCleanState = true; + } + /// /// Hold the start and end index of a pair of brackets /// @@ -1717,13 +1759,5 @@ namespace Avalonia.Media.TextFormatting.Unicode public BidiClass Eos { get; } } - - public void Dispose() - { - _workingClassesBuffer.Dispose(); - _resolvedLevelsBuffer.Dispose(); - _x9Map.Dispose(); - _isolatedRunMapping.Dispose(); - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 0f0b3235e1..5cc222b813 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,8 +11,10 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// - internal struct BidiData : IDisposable + /// To avoid allocations, this class is designed to be reused. + internal sealed class BidiData { + private bool _hasCleanState = true; private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; private ArrayBuilder _pairedBracketValues; @@ -20,12 +22,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private ArrayBuilder _savedPairedBracketTypes; private ArrayBuilder _tempLevelBuffer; - public BidiData(sbyte paragraphEmbeddingLevel) - { - ParagraphEmbeddingLevel = paragraphEmbeddingLevel; - } - - public sbyte ParagraphEmbeddingLevel { get; private set; } + public sbyte ParagraphEmbeddingLevel { get; set; } public bool HasBrackets { get; private set; } @@ -36,7 +33,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Gets the length of the data held by the BidiData /// - public int Length{get; private set; } + public int Length { get; private set; } /// /// Gets the bidi character type of each code point @@ -66,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text to process. public void Append(ReadOnlySpan text) { + _hasCleanState = false; + _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); _pairedBracketValues.Add(text.Length); @@ -151,6 +150,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void SaveTypes() { + _hasCleanState = false; + // Capture the types data _savedClasses.Clear(); _savedClasses.Add(_classes.AsSlice()); @@ -163,6 +164,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void RestoreTypes() { + _hasCleanState = false; + _classes.Clear(); _classes.Add(_savedClasses.AsSlice()); _pairedBracketTypes.Clear(); @@ -182,14 +185,34 @@ namespace Avalonia.Media.TextFormatting.Unicode return _tempLevelBuffer.Add(length, false); } - public void Dispose() + /// + /// Resets the bidi data to a clean state. + /// + public void Reset() { - _classes.Dispose(); - _pairedBracketTypes.Dispose(); - _pairedBracketValues.Dispose(); - _savedClasses.Dispose(); - _savedPairedBracketTypes.Dispose(); - _tempLevelBuffer.Dispose(); + if (_hasCleanState) + { + return; + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer); + + ParagraphEmbeddingLevel = 0; + HasBrackets = false; + HasEmbeddings = false; + HasIsolates = false; + Length = 0; + + Classes = default; + PairedBracketTypes = default; + PairedBracketValues = default; + + _hasCleanState = true; } } } diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 89bf047401..e2160f21d2 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; namespace Avalonia.Platform @@ -166,11 +167,9 @@ namespace Avalonia.Platform /// /// The glyph typeface. /// The font rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. - /// - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// The list of glyphs. + /// An . + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos); /// /// Creates a backend-specific object using a low-level API graphics context diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 18cb7a6308..32923a5257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -32,7 +32,7 @@ internal class FpsCounter { var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph }); } } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index b7b44a7dfe..093597c6a0 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -109,7 +109,7 @@ namespace Avalonia.Styling if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) { - return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!); + return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source); } throw new NotSupportedException(); diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index e6b67bd383..bbbcc39ecc 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Runtime.CompilerServices; namespace Avalonia.Utilities @@ -12,14 +11,13 @@ namespace Avalonia.Utilities /// A helper type for avoiding allocations while building arrays. /// /// The type of item contained in the array. - internal struct ArrayBuilder : IDisposable - where T : struct + internal struct ArrayBuilder { private const int DefaultCapacity = 4; private const int MaxCoreClrArrayLength = 0x7FeFFFFF; // Starts out null, initialized on first Add. - private T[] _data; + private T[]? _data; private int _size; /// @@ -49,6 +47,12 @@ namespace Avalonia.Utilities } } + /// + /// Gets the current capacity of the array. + /// + public int Capacity + => _data?.Length ?? 0; + /// /// Returns a reference to specified element of the array. /// @@ -116,14 +120,44 @@ namespace Avalonia.Utilities return slice; } + /// + /// Appends an item. + /// + /// The item to append. + public void AddItem(T value) + { + var index = Length++; + _data![index] = value; + } + /// /// Clears the array. /// Allocated memory is left intact for future usage. /// public void Clear() { - // No need to actually clear since we're not allowing reference types. +#if NET6_0_OR_GREATER + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ClearArray(); + } + else + { + _size = 0; + } +#else + ClearArray(); +#endif + } + + private void ClearArray() + { + var size = _size; _size = 0; + if (size > 0) + { + Array.Clear(_data!, 0, size); + } } private void EnsureCapacity(int min) @@ -136,7 +170,7 @@ namespace Avalonia.Utilities } // Same expansion algorithm as List. - var newCapacity = length == 0 ? DefaultCapacity : length * 2; + var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; if (newCapacity > MaxCoreClrArrayLength) { @@ -145,15 +179,14 @@ namespace Avalonia.Utilities if (newCapacity < min) { - newCapacity = min; + newCapacity = (uint)min; } - var array = ArrayPool.Shared.Rent(newCapacity); + var array = new T[newCapacity]; if (_size > 0) { Array.Copy(_data!, array, _size); - ArrayPool.Shared.Return(_data!); } _data = array; @@ -182,13 +215,12 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); - - public void Dispose() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } + + /// + /// Returns the current state of the array as a span. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AsSpan() => _data.AsSpan(0, _size); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index b70088a907..17e75f2f95 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -18,7 +17,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the slice. internal readonly struct ArraySlice : IReadOnlyList - where T : struct { /// /// Gets an empty @@ -186,13 +184,6 @@ namespace Avalonia.Utilities /// int IReadOnlyCollection.Count => Length; - - public void ReturnRent() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } + diff --git a/src/Avalonia.Base/Utilities/BidiDictionary.cs b/src/Avalonia.Base/Utilities/BidiDictionary.cs index 654fbc9807..01af53ad89 100644 --- a/src/Avalonia.Base/Utilities/BidiDictionary.cs +++ b/src/Avalonia.Base/Utilities/BidiDictionary.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media.TextFormatting; namespace Avalonia.Utilities { @@ -9,32 +11,27 @@ namespace Avalonia.Utilities /// Value type internal sealed class BidiDictionary where T1 : notnull where T2 : notnull { - public Dictionary Forward { get; } = new Dictionary(); + private Dictionary _forward = new(); + private Dictionary _reverse = new(); - public Dictionary Reverse { get; } = new Dictionary(); - - public void Clear() + public void ClearThenResetIfTooLarge() { - Forward.Clear(); - Reverse.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse); } public void Add(T1 key, T2 value) { - Forward.Add(key, value); - Reverse.Add(value, key); + _forward.Add(key, value); + _reverse.Add(value, key); } -#pragma warning disable CS8601 - public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value); -#pragma warning restore CS8601 + public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value); -#pragma warning disable CS8601 - public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key); -#pragma warning restore CS8601 + public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key); - public bool ContainsKey(T1 key) => Forward.ContainsKey(key); + public bool ContainsKey(T1 key) => _forward.ContainsKey(key); - public bool ContainsValue(T2 value) => Reverse.ContainsKey(value); + public bool ContainsValue(T2 value) => _reverse.ContainsKey(value); } } diff --git a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs index b7060d2e21..defc9b1639 100644 --- a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs +++ b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs @@ -14,7 +14,6 @@ // under the License. // Copied from: https://github.com/toptensoftware/RichTextKit -using System; using System.Collections.Generic; namespace Avalonia.Utilities @@ -39,7 +38,7 @@ namespace Avalonia.Utilities /// The value to search for /// The comparer /// The index of the found item; otherwise the bitwise complement of the index of the next larger item - public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) where T : IComparable + public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) { return list.BinarySearch(0, list.Count, value, comparer); } diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index e859a6e725..8f532b9803 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -7,6 +7,7 @@ using Avalonia.Data; using System; using Avalonia.Controls.Utils; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -111,9 +112,9 @@ namespace Avalonia.Controls if (result != null) { - if(result.Subject != null) + if(result.Source is IAvaloniaSubject subject) { - var bindingHelper = new CellEditBinding(result.Subject); + var bindingHelper = new CellEditBinding(subject); var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority); BindingOperations.Apply(target, property, instanceBinding, null); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index c1146cceda..10c2f36f43 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls { if(run.Length > 0) { -#if NET6_0 +#if NET6_0_OR_GREATER builder.Append(run.Text.Span); #else builder.Append(run.Text.Span.ToArray()); diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index fb20a7ee14..022e1a74b1 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -264,6 +264,18 @@ namespace Avalonia.Controls Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll } } + + /// + /// Invoked when the event occurs in the header. + /// + protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) + { + if (ItemCount > 0) + { + IsExpanded = !IsExpanded; + e.Handled = true; + } + } private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class { @@ -280,11 +292,7 @@ namespace Avalonia.Controls private void HeaderDoubleTapped(object? sender, TappedEventArgs e) { - if (ItemCount > 0) - { - IsExpanded = !IsExpanded; - e.Handled = true; - } + OnHeaderDoubleTapped(e); } } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index b066ba8141..fb53fd0172 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -9,6 +9,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Headless { @@ -119,7 +120,7 @@ namespace Avalonia.Headless return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new HeadlessGlyphRunStub(); } diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index e87f4b7d93..72a7797bc3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -92,6 +92,7 @@ + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding ItemTemplate}"> diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 9990ad4731..d0a4de09ab 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions }; } - protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + private protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { if (Source != null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index b7f2261324..2b62d33349 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.CompiledBindingSafeSupressWarningMessage)] - public ExpressionNode BuildExpression(bool enableValidation) + internal ExpressionNode BuildExpression(bool enableValidation) { ExpressionNode pathRoot = null; ExpressionNode path = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs index 1252ec7eca..b543fd9c01 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs @@ -3,7 +3,7 @@ using Avalonia.Data.Core; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { - public class StrongTypeCastNode : TypeCastNode + internal class StrongTypeCastNode : TypeCastNode { private Func _cast; diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index e3878b5bc6..ec44eeb38f 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 66907f33d0..37e7be5e3f 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -56,7 +56,7 @@ namespace Avalonia.Data /// public Func? TypeResolver { get; set; } - protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, bool enableDataValidation) + private protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, bool enableDataValidation) { _ = target ?? throw new ArgumentNullException(nameof(target)); diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 90f312a249..8c8c50763b 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -70,7 +70,7 @@ namespace Avalonia.Data public WeakReference? NameScope { get; set; } - protected abstract ExpressionObserver CreateExpressionObserver( + private protected abstract ExpressionObserver CreateExpressionObserver( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, @@ -127,7 +127,7 @@ namespace Avalonia.Data return new InstancedBinding(subject, Mode, Priority); } - protected ExpressionObserver CreateDataContextObserver( + private protected ExpressionObserver CreateDataContextObserver( AvaloniaObject target, ExpressionNode node, bool targetIsDataContext, @@ -162,7 +162,7 @@ namespace Avalonia.Data } } - protected ExpressionObserver CreateElementObserver( + private protected ExpressionObserver CreateElementObserver( StyledElement target, string elementName, ExpressionNode node) @@ -178,7 +178,7 @@ namespace Avalonia.Data return result; } - protected ExpressionObserver CreateFindAncestorObserver( + private protected ExpressionObserver CreateFindAncestorObserver( StyledElement target, RelativeSource relativeSource, ExpressionNode node) @@ -211,7 +211,7 @@ namespace Avalonia.Data null); } - protected ExpressionObserver CreateSourceObserver( + private protected ExpressionObserver CreateSourceObserver( object source, ExpressionNode node) { @@ -220,7 +220,7 @@ namespace Avalonia.Data return new ExpressionObserver(source, node); } - protected ExpressionObserver CreateTemplatedParentObserver( + private protected ExpressionObserver CreateTemplatedParentObserver( AvaloniaObject target, ExpressionNode node) { diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 1515ff2c90..993f63b4d3 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -85,8 +85,8 @@ namespace Avalonia.Data var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x?.Observable!) - .Where(x => x is not null) + var input = children.Select(x => x?.Source) + .Where(x => x is not null)! .CombineLatest() .Select(x => ConvertValue(x, targetType, converter)) .Where(x => x != BindingOperations.DoNothing); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 00f40dfcd3..97e9698c13 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -6,7 +6,7 @@ using Avalonia.Utilities; namespace Avalonia.Markup.Parsers { - public static class ExpressionObserverBuilder + internal static class ExpressionObserverBuilder { [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func? typeResolver = null, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 4fc17e440b..e676c74879 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -6,7 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { - public class ElementNameNode : ExpressionNode + internal class ElementNameNode : ExpressionNode { private readonly WeakReference _nameScope; private readonly string _name; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index ffbd34d492..383a160814 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -5,7 +5,7 @@ using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { - public class FindAncestorNode : ExpressionNode + internal class FindAncestorNode : ExpressionNode { private readonly int _level; private readonly Type? _ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs index 1cd233c68a..2cb87efa65 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - public class SelfNode : ExpressionNode + internal class SelfNode : ExpressionNode { public override string Description => "$self"; } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 23b7edcc8f..3fb7491898 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,11 +1,11 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using SkiaSharp; namespace Avalonia.Skia @@ -83,9 +83,9 @@ namespace Avalonia.Skia var (currentX, currentY) = glyphRun.BaselineOrigin; - for (var i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var glyph = glyphRun.GlyphIndices[i]; + var glyph = glyphRun.GlyphInfos[i].GlyphIndex; var glyphPath = skFont.GetGlyphPath(glyph); if (!glyphPath.IsEmpty) @@ -93,14 +93,7 @@ namespace Avalonia.Skia path.AddPath(glyphPath, (float)currentX, (float)currentY); } - if (glyphRun.GlyphAdvances != null) - { - currentX += glyphRun.GlyphAdvances[i]; - } - else - { - currentX += glyphPath.Bounds.Right; - } + currentX += glyphRun.GlyphInfos[i].GlyphAdvance; } return new StreamGeometryImpl(path); @@ -208,17 +201,16 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { if (glyphTypeface == null) { throw new ArgumentNullException(nameof(glyphTypeface)); } - if (glyphIndices == null) + if (glyphInfos == null) { - throw new ArgumentNullException(nameof(glyphIndices)); + throw new ArgumentNullException(nameof(glyphInfos)); } var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; @@ -237,59 +229,25 @@ namespace Avalonia.Skia var builder = new SKTextBlobBuilder(); - var count = glyphIndices.Count; - - if (glyphOffsets != null && glyphAdvances != null) - { - var runBuffer = builder.AllocatePositionedRun(font, count); - - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); + var count = glyphInfos.Count; - var currentX = 0.0; + var runBuffer = builder.AllocatePositionedRun(font, count); - for (int i = 0; i < glyphOffsets.Count; i++) - { - var offset = glyphOffsets[i]; + var glyphSpan = runBuffer.GetGlyphSpan(); + var positionSpan = runBuffer.GetPositionSpan(); - glyphSpan[i] = glyphIndices[i]; + var currentX = 0.0; - positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - - currentX += glyphAdvances[i]; - } - } - else + for (int i = 0; i < count; i++) { - if (glyphAdvances != null) - { - var runBuffer = builder.AllocateHorizontalRun(font, count, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); - - var currentX = 0.0; + var glyphInfo = glyphInfos[i]; + var offset = glyphInfo.GlyphOffset; - for (int i = 0; i < glyphAdvances.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; + glyphSpan[i] = glyphInfo.GlyphIndex; - positionSpan[i] = (float)currentX; + positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - currentX += glyphAdvances[i]; - } - } - else - { - var runBuffer = builder.AllocateRun(font, count, 0, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - - for (int i = 0; i < glyphIndices.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; - } - } + currentX += glyphInfo.GlyphAdvance; } return new GlyphRunImpl(builder.Build()); diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e0f95bac60..def2482af3 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Skia if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 25c351c908..32a637ce8b 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -7,6 +7,7 @@ using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; @@ -157,12 +158,11 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; - var glyphCount = glyphIndices.Count; + var glyphCount = glyphInfos.Count; var run = new SharpDX.DirectWrite.GlyphRun { @@ -174,44 +174,23 @@ namespace Avalonia.Direct2D1 for (var i = 0; i < glyphCount; i++) { - indices[i] = (short)glyphIndices[i]; + indices[i] = (short)glyphInfos[i].GlyphIndex; } run.Indices = indices; run.Advances = new float[glyphCount]; - var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight); - - if (glyphAdvances == null) - { - for (var i = 0; i < glyphCount; i++) - { - var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale; - - run.Advances[i] = advance; - } - } - else - { - for (var i = 0; i < glyphCount; i++) - { - var advance = (float)glyphAdvances[i]; - - run.Advances[i] = advance; - } - } - - if (glyphOffsets == null) + for (var i = 0; i < glyphCount; i++) { - return new GlyphRunImpl(run); + run.Advances[i] = (float)glyphInfos[i].GlyphAdvance; } run.Offsets = new GlyphOffset[glyphCount]; for (var i = 0; i < glyphCount; i++) { - var (x, y) = glyphOffsets[i]; + var (x, y) = glyphInfos[i].GlyphOffset; run.Offsets[i] = new GlyphOffset { @@ -255,11 +234,12 @@ namespace Avalonia.Direct2D1 using (var sink = pathGeometry.Open()) { - var glyphs = new short[glyphRun.GlyphIndices.Count]; + var glyphInfos = glyphRun.GlyphInfos; + var glyphs = new short[glyphInfos.Count]; - for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (int i = 0; i < glyphInfos.Count; i++) { - glyphs[i] = (short)glyphRun.GlyphIndices[i]; + glyphs[i] = (short)glyphInfos[i].GlyphIndex; } glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index fffa5ce490..ac441108e3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Direct2D1.Media if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 363fb3f5b3..3573ba6b07 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System; +using System.Linq; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Utilities; @@ -179,12 +181,14 @@ namespace Avalonia.Base.UnitTests.Media private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0) { var count = glyphAdvances.Length; - var glyphIndices = new ushort[count]; - var characters = Enumerable.Repeat('a', count).ToArray(); + var glyphInfos = new GlyphInfo[count]; + for (var i = 0; i < count; ++i) + { + glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); + } - return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, - glyphClusters: glyphClusters, biDiLevel: bidiLevel); + return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel); } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index 9d189d1950..eb69bed1e1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -1,8 +1,6 @@ -using System; -using System.Linq; +using System.Linq; using System.Runtime.InteropServices; using System.Text; -using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -32,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private bool Run(BiDiClassData t) { var bidi = new BidiAlgorithm(); - var bidiData = new BidiData(t.ParagraphLevel); + var bidiData = new BidiData { ParagraphEmbeddingLevel = t.ParagraphLevel }; var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs new file mode 100644 index 0000000000..192f34eea7 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.TextFormatting +{ + public class FormattingBufferHelperTests + { + public static TheoryData SmallSizes => new() { 1, 500, 10_000, 125_000 }; + public static TheoryData LargeSizes => new() { 500_000, 1_000_000 }; + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearList(int itemCount) + { + var list = new List(); + + for (var i = 0; i < itemCount; ++i) + { + list.Add(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(list); + + return list.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearArrayBuilder(int itemCount) + { + var arrayBuilder = new ArrayBuilder(); + + for (var i = 0; i < itemCount; ++i) + { + arrayBuilder.AddItem(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref arrayBuilder); + + return arrayBuilder.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearStack(int itemCount) + { + var stack = new Stack(); + + for (var i = 0; i < itemCount; ++i) + { + stack.Push(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(stack); + + var array = (Array) stack.GetType() + .GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(stack)!; + + return array.Length; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity <= 3); // dictionary trims to the nearest prime starting with 3 + } + + private static int FillAndClearDictionary(int itemCount) + { + var dictionary = new Dictionary(); + + for (var i = 0; i < itemCount; ++i) + { + dictionary.Add(i, i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref dictionary); + + var array = (Array) dictionary.GetType() + .GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(dictionary)!; + + return array.Length; + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 6e0d7f657a..ee501a86c1 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Base.UnitTests.VisualTree { @@ -76,7 +77,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 5b28453c17..6c08cc578e 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks @@ -122,7 +123,7 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new MockGlyphRun(); } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index bf0253e9ab..c96edbef5c 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -9,6 +9,8 @@ using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Text; [MemoryDiagnoser] +[MinIterationTime(150)] +[MaxWarmupCount(15)] public class HugeTextLayout : IDisposable { private readonly IDisposable _app; @@ -44,7 +46,13 @@ Admitting that the possibility of achieving the results of the constructive crit Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; - + + [Params(false, true)] + public bool Wrap { get; set; } + + [Params(false, true)] + public bool Trim { get; set; } + [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -91,9 +99,12 @@ In respect that the structure of the sufficient amount poses problems and challe } } - private static TextLayout MakeLayout(string str) + private TextLayout MakeLayout(string str) { - var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120); + var wrapping = Wrap ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = Trim ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, + textTrimming: trimming, textWrapping: wrapping); layout.Dispose(); return layout; } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 656c2cbbbc..3ba8e8354d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -334,7 +334,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.IsType(((BindingExpression)result).Converter); } @@ -350,7 +350,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.Same(converter.Object, ((BindingExpression)result).Converter); } @@ -367,7 +367,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Bar", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.Same("foo", ((BindingExpression)result).ConverterParameter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs index 2a0750b131..680c49d098 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TextProperty).Observable; + TextBlock.TextProperty).Source; Assert.Same(StringConverters.IsNullOrEmpty, expressionObserver.Converter); } @@ -46,7 +46,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TextProperty).Observable; + TextBlock.TextProperty).Source; Assert.IsType(expressionObserver.Converter); } @@ -69,7 +69,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TagProperty).Observable; + TextBlock.TagProperty).Source; Assert.IsType(expressionObserver.Converter); } @@ -92,7 +92,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.MarginProperty).Observable; + TextBlock.MarginProperty).Source; Assert.Same(DefaultValueConverter.Instance, expressionObserver.Converter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs index 45deb97f51..505eddb146 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); @@ -56,7 +56,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template }; var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index a7ef2c4e4d..bf9631760a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.UnitTests.Data }; var target = new Control { DataContext = source }; - var observable = binding.Initiate(target, null).Observable; + var observable = binding.Initiate(target, null).Source; var result = await observable.Take(1); Assert.Equal("1,2,3", result); @@ -59,7 +59,7 @@ namespace Avalonia.Markup.UnitTests.Data }; var target = new Control { DataContext = source }; - var observable = binding.Initiate(target, null).Observable; + var observable = binding.Initiate(target, null).Source; var result = await observable.Take(1); Assert.Equal("1,2,3", result); diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 31e485448e..772d6e1023 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Documents; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Xunit; #if AVALONIA_SKIA @@ -170,11 +171,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale; - var advances = new[] { advance, advance, advance}; + var glyphInfos = new[] + { + new GlyphInfo(glyphIndices[0], 0, advance), + new GlyphInfo(glyphIndices[1], 1, advance), + new GlyphInfo(glyphIndices[2], 2, advance) + }; var characters = new[] { 'A', 'B', 'C' }; - GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices, advances); + GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphInfos); } public GlyphRun GlyphRun { get; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 04bc401479..f2d6670be5 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -134,11 +134,11 @@ namespace Avalonia.Skia.UnitTests.Media foreach (var rect in rects) { - var currentCluster = glyphRun.GlyphClusters[index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphClusters.Count) + while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphInfos.Count) { - currentCluster = glyphRun.GlyphClusters[++index]; + currentCluster = glyphRun.GlyphInfos[++index].GlyphCluster; } //Non trailing edge @@ -161,15 +161,15 @@ namespace Avalonia.Skia.UnitTests.Media var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace; - var rects = new List(glyphRun.GlyphAdvances!.Count); + var rects = new List(glyphRun.GlyphInfos!.Count); var lastCluster = -1; - for (var index = 0; index < glyphRun.GlyphAdvances.Count; index++) + for (var index = 0; index < glyphRun.GlyphInfos.Count; index++) { - var currentCluster = glyphRun.GlyphClusters![index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - var advance = glyphRun.GlyphAdvances[index]; + var advance = glyphRun.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { @@ -216,10 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, shapedBuffer.Text, - shapedBuffer.GlyphIndices, - shapedBuffer.GlyphAdvances, - shapedBuffer.GlyphOffsets, - shapedBuffer.GlyphClusters, + shapedBuffer.GlyphInfos, shapedBuffer.BidiLevel); if(shapedBuffer.BidiLevel == 1) @@ -233,7 +230,7 @@ namespace Avalonia.Skia.UnitTests.Media private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index b90752861c..6b9fb579b1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -585,7 +585,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedRuns = expectedTextLine.TextRuns.Cast().ToList(); - var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var expectedGlyphs = expectedRuns + .SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); for (var i = 0; i < text.Length; i++) { @@ -604,7 +606,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var actualGlyphs = shapedRuns + .SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); Assert.Equal(expectedGlyphs, actualGlyphs); } @@ -706,7 +710,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl())); AvaloniaLocator.CurrentMutable diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 1fd26748cd..a24a0fcf70 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -142,8 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting black, textWrapping: TextWrapping.Wrap); - var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var expectedGlyphs = GetGlyphs(expected); var outer = new GraphemeEnumerator(text); var inner = new GraphemeEnumerator(text); @@ -175,8 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textWrapping: TextWrapping.Wrap, textStyleOverrides: spans); - var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var actualGlyphs = GetGlyphs(actual); Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count); @@ -196,6 +194,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting i += outer.Current.Text.Length; } } + + static List GetGlyphs(TextLayout textLayout) + => textLayout.TextLines + .Select(line => string.Join('|', line.TextRuns + .Cast() + .SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex))) + .ToList(); } [Fact] @@ -484,13 +489,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters; + var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray(); - var expected = clusters.Skip(index).Take(glyphClusters.Count).ToArray(); + var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray(); Assert.Equal(expected, glyphClusters); - index += glyphClusters.Count; + index += glyphClusters.Length; } } } @@ -515,13 +520,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, layout.TextLines[0].TextRuns.Count); - Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count); + Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count); - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster); if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster); } } } @@ -562,9 +567,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); - foreach (var glyph in textRun.GlyphRun.GlyphIndices) + foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos) { - Assert.Equal(replacementGlyph, glyph); + Assert.Equal(replacementGlyph, glyphInfo.GlyphIndex); } } } @@ -776,8 +781,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width); } - var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast()) - .SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray(); + var rects = layout.TextLines + .SelectMany(x => x.TextRuns.Cast()) + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance) + .ToArray(); for (var i = 0; i < SingleLineText.Length; i++) { @@ -865,10 +872,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentX = 0.0; - for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < firstRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = firstRun.GlyphRun.GlyphClusters[i]; - var advance = firstRun.GlyphRun.GlyphAdvances[i]; + var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -895,10 +902,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentX = firstRun.Size.Width + 0.5; - for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < secondRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = secondRun.GlyphRun.GlyphClusters[i]; - var advance = secondRun.GlyphRun.GlyphAdvances[i]; + var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -932,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = (ShapedTextRun)textLine.TextRuns[0]; - var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; + var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster; var characterHit = textLine.GetCharacterHitFromDistance(0); @@ -946,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); - var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; + var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance; Assert.Equal(firstAdvance, distance, 5); @@ -991,9 +998,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); + var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList(); - var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList(); + var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList(); var currentX = 0.0; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 6993c70e8b..544b84912e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -95,9 +95,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); @@ -142,9 +142,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } clusters.Reverse(); @@ -247,7 +247,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) + var clusters = textLine.TextRuns + .Cast() + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster) .ToArray(); var previousCharacterHit = new CharacterHit(text.Length); @@ -313,11 +315,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = textRun.GlyphRun; - for (var i = 0; i < glyphRun.GlyphClusters!.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var cluster = glyphRun.GlyphClusters[i]; + var cluster = glyphRun.GlyphInfos[i].GlyphCluster; - var advance = glyphRun.GlyphAdvances[i]; + var advance = glyphRun.GlyphInfos[i].GlyphAdvance; var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); @@ -750,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - var currentClusters = shapedBuffer.GlyphClusters.ToList(); + var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList(); foreach (var currentCluster in currentClusters) { @@ -783,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) + for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++) { - var currentCluster = shapedBuffer.GlyphClusters[index]; + var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster; - var advance = shapedBuffer.GlyphAdvances[index]; + var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 834dce4a90..3f02867aa9 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -19,10 +19,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); - Assert.Equal(0, shapedBuffer.GlyphClusters[0]); - Assert.Equal(1, shapedBuffer.GlyphClusters[1]); - Assert.Equal(1, shapedBuffer.GlyphClusters[2]); + Assert.Equal(shapedBuffer.GlyphInfos.Length, text.Length); + Assert.Equal(0, shapedBuffer.GlyphInfos[0].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[1].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[2].GlyphCluster); } } @@ -36,7 +36,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(100, shapedBuffer.GlyphAdvances[0]); + Assert.Equal(100, shapedBuffer.GlyphInfos[0].GlyphAdvance); } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 566cb0f1ac..baf5ffb07c 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.UnitTests if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 143937307e..57a877beb3 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -4,6 +4,7 @@ using System.IO; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Rendering; using Moq; @@ -148,7 +149,7 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return Mock.Of(); } diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 3218139251..b5f4777192 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -24,7 +24,10 @@ namespace Avalonia.UnitTests var glyphIndex = typeface.GetGlyph(codepoint); - shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10); + for (var j = 0; j < count; ++j) + { + shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); + } i += count; } diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 407b67b8a0..547e9b2763 100644 Binary files a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png and b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png differ