diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs
index 4edb0f137ad..caad8b0854b 100644
--- a/samples/ControlCatalog/Pages/ScreenPage.cs
+++ b/samples/ControlCatalog/Pages/ScreenPage.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
@@ -49,25 +50,33 @@ public override void Render(DrawingContext context)
context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect);
- var text = new FormattedText() { Typeface = new Typeface("Arial"), FontSize = 18 };
- text.Text = $"Bounds: {screen.Bounds.TopLeft} {screen.Bounds.Width}:{screen.Bounds.Height}";
- context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text);
-
- text.Text = $"WorkArea: {screen.WorkingArea.TopLeft} {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
- context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
+ var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
+ context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
- text.Text = $"Scaling: {screen.PixelDensity * 100}%";
- context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text);
-
- text.Text = $"Primary: {screen.Primary}";
- context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text);
-
- text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}";
- context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
+ formattedText =
+ CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
+ context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
+
+ formattedText = CreateFormattedText($"Scaling: {screen.PixelDensity * 100}%");
+ context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
+
+ formattedText = CreateFormattedText($"Primary: {screen.Primary}");
+ context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
+
+ formattedText =
+ CreateFormattedText(
+ $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}");
+ context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
}
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
}
+
+ private FormattedText CreateFormattedText(string textToFormat)
+ {
+ return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
+ Typeface.Default, 12, Brushes.Green);
+ }
}
}
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index a4c6299278f..4a8fb819cab 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -57,6 +57,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs
index 2e59d934a14..9c524a79320 100644
--- a/samples/RenderDemo/Pages/CustomSkiaPage.cs
+++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@@ -41,7 +42,10 @@ public void Render(IDrawingContextImpl context)
{
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
if (canvas == null)
- context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl);
+ using (var c = new DrawingContext(context, false))
+ {
+ c.DrawText(_noSkia, new Point());
+ }
else
{
canvas.Save();
@@ -108,10 +112,9 @@ static int Animate(int d, int from, int to)
public override void Render(DrawingContext context)
{
- var noSkia = new FormattedText()
- {
- Text = "Current rendering API is not Skia"
- };
+ var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black);
+
context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
}
diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml b/samples/RenderDemo/Pages/FormattedTextPage.axaml
new file mode 100644
index 00000000000..92775bec9e9
--- /dev/null
+++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml
@@ -0,0 +1,7 @@
+
+
diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
new file mode 100644
index 00000000000..25e29c67a9a
--- /dev/null
+++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
@@ -0,0 +1,60 @@
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+
+namespace RenderDemo.Pages
+{
+ public class FormattedTextPage : UserControl
+ {
+ public FormattedTextPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ const string testString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor";
+
+ // Create the initial formatted text string.
+ var formattedText = new FormattedText(
+ testString,
+ CultureInfo.GetCultureInfo("en-us"),
+ FlowDirection.LeftToRight,
+ new Typeface("Verdana"),
+ 32,
+ Brushes.Black) { MaxTextWidth = 300, MaxTextHeight = 240 };
+
+ // Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears.
+
+ // Use a larger font size beginning at the first (zero-based) character and continuing for 5 characters.
+ // The font size is calculated in terms of points -- not as device-independent pixels.
+ formattedText.SetFontSize(36 * (96.0 / 72.0), 0, 5);
+
+ // Use a Bold font weight beginning at the 6th character and continuing for 11 characters.
+ formattedText.SetFontWeight(FontWeight.Bold, 6, 11);
+
+ var gradient = new LinearGradientBrush
+ {
+ GradientStops =
+ new GradientStops { new GradientStop(Colors.Orange, 0), new GradientStop(Colors.Teal, 1) },
+ StartPoint = new RelativePoint(0,0, RelativeUnit.Relative),
+ EndPoint = new RelativePoint(0,1, RelativeUnit.Relative)
+ };
+
+ // Use a linear gradient brush beginning at the 6th character and continuing for 11 characters.
+ formattedText.SetForegroundBrush(gradient, 6, 11);
+
+ // Use an Italic font style beginning at the 28th character and continuing for 28 characters.
+ formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
+
+ context.DrawText(formattedText, new Point(10, 0));
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
index 857358f6b26..7f856069576 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -13,6 +13,7 @@ public class GlyphRunPage : UserControl
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
+ private char[] _characters = new char[1];
private float _fontSize = 20;
private int _direction = 10;
@@ -38,7 +39,7 @@ private void InitializeComponent()
private void UpdateGlyphRun()
{
- var c = (uint)_rand.Next(65, 90);
+ var c = (char)_rand.Next(65, 90);
if (_fontSize + _direction > 200)
{
@@ -54,6 +55,8 @@ private void UpdateGlyphRun()
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+ _characters[0] = c;
+
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
var drawingGroup = new DrawingGroup();
@@ -61,7 +64,7 @@ private void UpdateGlyphRun()
var glyphRunDrawing = new GlyphRunDrawing
{
Foreground = Brushes.Black,
- GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
+ GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
};
drawingGroup.Children.Add(glyphRunDrawing);
diff --git a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
index 90d1c52ff53..251dfe43510 100644
--- a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
+++ b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
@@ -3,7 +3,7 @@
namespace Avalonia.Utilities
{
- public struct ImmutableReadOnlyListStructEnumerator : IEnumerator, IEnumerator
+ public struct ImmutableReadOnlyListStructEnumerator : IEnumerator
{
private readonly IReadOnlyList _readOnlyList;
private int _pos;
diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt
index a7560c37f24..2c206b53f65 100644
--- a/src/Avalonia.Controls/ApiCompatBaseline.txt
+++ b/src/Avalonia.Controls/ApiCompatBaseline.txt
@@ -43,6 +43,10 @@ MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.Off
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'protected Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.CreateFormattedText()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.FormattedText.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Int32 Avalonia.Controls.Presenters.TextPresenter.GetCaretIndex(Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.TextPresenter.InvalidateFormattedText()' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
@@ -63,4 +67,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
-Total Issues: 64
+Total Issues: 68
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index 441421181cf..35648dd0b62 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -1,10 +1,12 @@
using System;
using System.ComponentModel;
+using System.Runtime.CompilerServices;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
+using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -60,7 +62,13 @@ public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, I
public static readonly RoutedEvent ContextRequestedEvent =
RoutedEvent.Register(nameof(ContextRequested),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
-
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty FlowDirectionProperty =
+ AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true);
+
private DataTemplates? _dataTemplates;
private IControl? _focusAdorner;
@@ -108,6 +116,15 @@ public object? Tag
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
+
+ ///
+ /// Gets or sets the text flow direction.
+ ///
+ public FlowDirection FlowDirection
+ {
+ get => GetValue(FlowDirectionProperty);
+ set => SetValue(FlowDirectionProperty, value);
+ }
///
/// Occurs when the user has completed a context input gesture, such as a right-click.
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index ff63e5644f6..8bbc7a9d279 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -1,9 +1,12 @@
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Reactive.Linq;
-using Avalonia.Input.TextInput;
using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Threading;
+using Avalonia.Utilities;
using Avalonia.VisualTree;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
@@ -75,28 +78,18 @@ public class TextPresenter : Control
private int _selectionEnd;
private bool _caretBlink;
private string _text;
- private FormattedText _formattedText;
- private Size _constraint;
+ private TextLayout _textLayout;
+ private Size _constraint = Size.Infinity;
- static TextPresenter()
- {
- AffectsRender(SelectionBrushProperty, TextBlock.ForegroundProperty,
- SelectionForegroundBrushProperty, CaretBrushProperty,
- SelectionStartProperty, SelectionEndProperty);
-
- AffectsMeasure(TextProperty, PasswordCharProperty, RevealPasswordProperty,
- TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
- TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
+ private CharacterHit _lastCharacterHit;
+ private Rect _caretBounds;
+ private Point _navigationPosition;
- Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
- TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
- TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
- TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
- SelectionStartProperty.Changed, SelectionEndProperty.Changed,
- SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed, RevealPasswordProperty.Changed
- ).AddClassHandler((x, _) => x.InvalidateFormattedText());
+ private ScrollViewer _scrollViewer;
- CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue));
+ static TextPresenter()
+ {
+ AffectsRender(CaretBrushProperty, SelectionBrushProperty);
}
public TextPresenter()
@@ -106,6 +99,8 @@ public TextPresenter()
_caretTimer.Tick += CaretTimerTick;
}
+ public event EventHandler CaretBoundsChanged;
+
///
/// Gets or sets a brush used to paint the control's background.
///
@@ -189,13 +184,22 @@ public TextAlignment TextAlignment
}
///
- /// Gets the used to render the text.
+ /// Gets the used to render the text.
///
- public FormattedText FormattedText
+ public TextLayout TextLayout
{
get
{
- return _formattedText ?? (_formattedText = CreateFormattedText());
+ if (_textLayout != null)
+ {
+ return _textLayout;
+ }
+
+ _textLayout = CreateTextLayout();
+
+ UpdateCaret(_lastCharacterHit);
+
+ return _textLayout;
}
}
@@ -205,11 +209,12 @@ public int CaretIndex
{
return _caretIndex;
}
-
set
{
- value = CoerceCaretIndex(value);
- SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
+ if (value != _caretIndex)
+ {
+ MoveCaretToTextPosition(value);
+ }
}
}
@@ -271,37 +276,25 @@ public int SelectionEnd
}
}
- public int GetCaretIndex(Point point)
- {
- var hit = FormattedText.HitTestPoint(point);
- return hit.TextPosition + (hit.IsTrailing ? 1 : 0);
- }
-
///
- /// Creates the used to render the text.
+ /// Creates the used to render the text.
///
/// The constraint of the text.
/// The text to format.
- /// A object.
- private FormattedText CreateFormattedTextInternal(Size constraint, string text)
- {
- return new FormattedText
- {
- Constraint = constraint,
- Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
- FontSize = FontSize,
- Text = text ?? string.Empty,
- TextAlignment = TextAlignment,
- TextWrapping = TextWrapping,
- };
- }
+ ///
+ ///
+ /// A object.
+ private TextLayout CreateTextLayoutInternal(Size constraint, string text, Typeface typeface,
+ IReadOnlyList> textStyleOverrides)
+ {
+ var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
+ var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height;
+
+ var textLayout = new TextLayout(text ?? string.Empty, typeface, FontSize, Foreground, TextAlignment,
+ TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
+ flowDirection: FlowDirection);
- ///
- /// Invalidates .
- ///
- protected void InvalidateFormattedText()
- {
- _formattedText = null;
+ return textLayout;
}
///
@@ -317,31 +310,36 @@ private void RenderInternal(DrawingContext context)
context.FillRectangle(background, new Rect(Bounds.Size));
}
- double top = 0;
- var textSize = FormattedText.Bounds.Size;
+ var top = 0d;
+ var left = 0.0;
+
+ var (_, textHeight) = TextLayout.Size;
- if (Bounds.Height < textSize.Height)
+ if (Bounds.Height < textHeight)
{
switch (VerticalAlignment)
{
case VerticalAlignment.Center:
- top += (Bounds.Height - textSize.Height) / 2;
+ top += (Bounds.Height - textHeight) / 2;
break;
case VerticalAlignment.Bottom:
- top += (Bounds.Height - textSize.Height);
+ top += (Bounds.Height - textHeight);
break;
}
}
- context.DrawText(Foreground, new Point(0, top), FormattedText);
+ TextLayout.Draw(context, new Point(left, top));
}
public override void Render(DrawingContext context)
{
- FormattedText.Constraint = Bounds.Size;
-
- _constraint = Bounds.Size;
+ if (double.IsPositiveInfinity (_constraint.Width))
+ {
+ _constraint = _scrollViewer?.Viewport ?? Size.Infinity;
+
+ InvalidateTextLayout();
+ }
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
@@ -351,7 +349,7 @@ public override void Render(DrawingContext context)
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
- var rects = FormattedText.HitTestTextRange(start, length);
+ var rects = TextLayout.HitTestTextRange(start, length);
foreach (var rect in rects)
{
@@ -361,40 +359,47 @@ public override void Render(DrawingContext context)
RenderInternal(context);
- if (selectionStart == selectionEnd && _caretBlink)
+ if (selectionStart != selectionEnd || !_caretBlink)
+ {
+ return;
+ }
+
+ var caretBrush = CaretBrush?.ToImmutable();
+
+ if (caretBrush is null)
{
- var caretBrush = CaretBrush?.ToImmutable();
+ var backgroundColor = (Background as ISolidColorBrush)?.Color;
- if (caretBrush is null)
+ if (backgroundColor.HasValue)
{
- var backgroundColor = (Background as ISolidColorBrush)?.Color;
- if (backgroundColor.HasValue)
- {
- byte red = (byte)~(backgroundColor.Value.R);
- byte green = (byte)~(backgroundColor.Value.G);
- byte blue = (byte)~(backgroundColor.Value.B);
+ var red = (byte)~(backgroundColor.Value.R);
+ var green = (byte)~(backgroundColor.Value.G);
+ var blue = (byte)~(backgroundColor.Value.B);
- caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue));
- }
- else
- {
- caretBrush = Brushes.Black;
- }
+ caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue));
+ }
+ else
+ {
+ caretBrush = Brushes.Black;
}
-
- var (p1, p2) = GetCaretPoints();
- context.DrawLine(
- new ImmutablePen(caretBrush, 1),
- p1, p2);
}
- }
- (Point, Point) GetCaretPoints()
+ var (p1, p2) = GetCaretPoints();
+
+ context.DrawLine(new ImmutablePen(caretBrush), p1, p2);
+ }
+
+ private (Point, Point) GetCaretPoints()
{
- var charPos = FormattedText.HitTestTextPosition(CaretIndex);
- var x = Math.Floor(charPos.X) + 0.5;
- var y = Math.Floor(charPos.Y) + 0.5;
- var b = Math.Ceiling(charPos.Bottom) - 0.5;
+ var x = Math.Floor(_caretBounds.X) + 0.5;
+ var y = Math.Floor(_caretBounds.Y) + 0.5;
+ var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
+
+ if (x >= Bounds.Width)
+ {
+ x = Math.Floor(_caretBounds.X - 1) + 0.5;
+ }
+
return (new Point(x, y), new Point(x, b));
}
@@ -412,7 +417,7 @@ public void HideCaret()
InvalidateVisual();
}
- internal void CaretIndexChanged(int caretIndex)
+ internal void CaretChanged()
{
if (this.GetVisualParent() != null)
{
@@ -432,8 +437,7 @@ internal void CaretIndexChanged(int caretIndex)
if (IsMeasureValid)
{
- var rect = FormattedText.HitTestTextPosition(caretIndex);
- this.BringIntoView(rect);
+ this.BringIntoView(_caretBounds);
}
else
{
@@ -443,8 +447,7 @@ internal void CaretIndexChanged(int caretIndex)
Dispatcher.UIThread.Post(
() =>
{
- var rect = FormattedText.HitTestTextPosition(caretIndex);
- this.BringIntoView(rect);
+ this.BringIntoView(_caretBounds);
},
DispatcherPriority.Render);
}
@@ -452,104 +455,310 @@ internal void CaretIndexChanged(int caretIndex)
}
///
- /// Creates the used to render the text.
+ /// Creates the used to render the text.
///
- /// A object.
- protected virtual FormattedText CreateFormattedText()
+ /// A object.
+ protected virtual TextLayout CreateTextLayout()
{
- FormattedText result = null;
+ TextLayout result;
var text = Text;
- if (PasswordChar != default(char) && !RevealPassword)
- {
- result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
- }
- else
- {
- result = CreateFormattedTextInternal(_constraint, text);
- }
+ var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
+ IReadOnlyList> textStyleOverrides = null;
+
if (length > 0)
{
- result.Spans = new[]
+ textStyleOverrides = new[]
{
- new FormattedTextStyleSpan(start, length, SelectionForegroundBrush),
+ new ValueSpan(start, length,
+ new GenericTextRunProperties(typeface, FontSize,
+ foregroundBrush: SelectionForegroundBrush ?? Brushes.White))
};
}
+ if (PasswordChar != default(char) && !RevealPassword)
+ {
+ result = CreateTextLayoutInternal(_constraint, new string(PasswordChar, text?.Length ?? 0), typeface,
+ textStyleOverrides);
+ }
+ else
+ {
+ result = CreateTextLayoutInternal(_constraint, text, typeface, textStyleOverrides);
+ }
+
return result;
}
- ///
- /// Measures the control.
- ///
- /// The available size for the control.
- /// The desired size.
- private Size MeasureInternal(Size availableSize)
+ protected virtual void InvalidateTextLayout()
+ {
+ _textLayout = null;
+
+ InvalidateMeasure();
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint)
+ {
+ _constraint = availableSize;
+
+ InvalidateTextLayout();
+ }
+
+ return TextLayout.Size;
+ }
+
+ private int CoerceCaretIndex(int value)
+ {
+ var text = Text;
+ var length = text?.Length ?? 0;
+ return Math.Max(0, Math.Min(length, value));
+ }
+
+ private void CaretTimerTick(object sender, EventArgs e)
+ {
+ _caretBlink = !_caretBlink;
+ InvalidateVisual();
+ }
+
+ public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)
+ {
+ var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge);
+ var textLine = TextLayout.TextLines[lineIndex];
+
+ var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition));
+
+ var nextCaretCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
+
+ if (nextCaretCharacterHit.FirstCharacterIndex <= textPosition)
+ {
+ characterHit = nextCaretCharacterHit;
+ }
+
+ if (textPosition == characterHit.FirstCharacterIndex + characterHit.TrailingLength)
+ {
+ UpdateCaret(characterHit);
+ }
+ else
+ {
+ UpdateCaret(trailingEdge ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex));
+ }
+
+ _navigationPosition = _caretBounds.Position;
+ }
+
+ public void MoveCaretToPoint(Point point)
+ {
+ var hit = TextLayout.HitTestPoint(point);
+
+ UpdateCaret(hit.CharacterHit);
+
+ _navigationPosition = _caretBounds.Position;
+ }
+
+ public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
{
- if (!string.IsNullOrEmpty(Text))
+ var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0);
+
+ if (lineIndex < 0)
+ {
+ return;
+ }
+
+ var (currentX, currentY) = _navigationPosition;
+
+ if (direction == LogicalDirection.Forward)
{
- if (TextWrapping == TextWrapping.Wrap)
+ if (lineIndex + 1 > TextLayout.TextLines.Count - 1)
{
- _constraint = new Size(availableSize.Width, double.PositiveInfinity);
+ return;
}
- else
+
+ var textLine = TextLayout.TextLines[lineIndex];
+
+ currentY += textLine.Height;
+ }
+ else
+ {
+ if (lineIndex - 1 < 0)
{
- _constraint = Size.Infinity;
+ return;
}
- _formattedText = null;
+ var textLine = TextLayout.TextLines[--lineIndex];
- return FormattedText.Bounds.Size;
+ currentY -= textLine.Height;
}
- return new Size();
+ var navigationPosition = _navigationPosition;
+
+ MoveCaretToPoint(new Point(currentX, currentY));
+
+ _navigationPosition = navigationPosition.WithY(_caretBounds.Y);
}
- protected override Size MeasureOverride(Size availableSize)
+ public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
{
- var text = Text;
+ var characterHit = _lastCharacterHit;
+ var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
- if (!string.IsNullOrEmpty(text))
+ if (lineIndex < 0)
{
- return MeasureInternal(availableSize);
+ return;
+ }
+
+ if (direction == LogicalDirection.Forward)
+ {
+ while (lineIndex < TextLayout.TextLines.Count)
+ {
+ var textLine = TextLayout.TextLines[lineIndex];
+
+ characterHit = textLine.GetNextCaretCharacterHit(characterHit);
+
+ caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length)
+ {
+ characterHit = new CharacterHit(caretIndex);
+ }
+
+ if (caretIndex >= Text.Length)
+ {
+ characterHit = new CharacterHit(Text.Length);
+
+ break;
+ }
+
+ if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length)
+ {
+ break;
+ }
+
+ if (caretIndex <= CaretIndex)
+ {
+ lineIndex++;
+
+ continue;
+ }
+
+ break;
+ }
}
else
{
- return new FormattedText
+ while (lineIndex >= 0)
{
- Text = "X",
- Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
- FontSize = FontSize,
- TextAlignment = TextAlignment,
- Constraint = availableSize,
- }.Bounds.Size;
+ var textLine = TextLayout.TextLines[lineIndex];
+
+ characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
+
+ caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ if (caretIndex >= CaretIndex)
+ {
+ lineIndex--;
+
+ continue;
+ }
+
+ break;
+ }
}
+
+ UpdateCaret(characterHit);
+
+ _navigationPosition = _caretBounds.Position;
}
- private int CoerceCaretIndex(int value)
+ private void UpdateCaret(CharacterHit characterHit)
{
- var text = Text;
- var length = text?.Length ?? 0;
- return Math.Max(0, Math.Min(length, value));
+ _lastCharacterHit = characterHit;
+
+ var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0);
+ var textLine = TextLayout.TextLines[lineIndex];
+ var distanceX = textLine.GetDistanceFromCharacterHit(characterHit);
+
+ var distanceY = 0d;
+
+ for (var i = 0; i < lineIndex; i++)
+ {
+ var currentLine = TextLayout.TextLines[i];
+
+ distanceY += currentLine.Height;
+ }
+
+ var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height);
+
+ if (caretBounds != _caretBounds)
+ {
+ _caretBounds = caretBounds;
+
+ CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ CaretChanged();
+
+ SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
}
- private void CaretTimerTick(object sender, EventArgs e)
+ internal Rect GetCursorRectangle()
{
- _caretBlink = !_caretBlink;
- InvalidateVisual();
+ return _caretBounds;
}
- internal Rect GetCursorRectangle()
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
- var (p1, p2) = GetCaretPoints();
- return new Rect(p1, p2);
+ base.OnAttachedToVisualTree(e);
+
+ _scrollViewer = this.FindAncestorOfType();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+
+ _scrollViewer = null;
+
+ _caretTimer.Stop();
+
+ _caretTimer.Tick -= CaretTimerTick;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ switch (change.Property.Name)
+ {
+ case nameof (TextBlock.Foreground):
+ case nameof (TextBlock.FontSize):
+ case nameof (TextBlock.FontStyle):
+ case nameof (TextBlock.FontWeight):
+ case nameof (TextBlock.FontFamily):
+ case nameof (Text):
+ case nameof (TextAlignment):
+ case nameof (TextWrapping):
+ case nameof (SelectionStart):
+ case nameof (SelectionEnd):
+ case nameof (SelectionForegroundBrush):
+ case nameof (PasswordChar):
+ case nameof (RevealPassword):
+ {
+ InvalidateTextLayout();
+ break;
+ }
+ }
}
}
}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 14cde774f46..8291433d45e 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -131,21 +131,8 @@ public class TextBlock : Control
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue(true);
-
- AffectsRender(BackgroundProperty, ForegroundProperty,
- TextAlignmentProperty, TextDecorationsProperty);
-
- AffectsMeasure(FontSizeProperty, FontWeightProperty,
- FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
- TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty);
-
- Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed,
- TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
- TextTrimmingProperty.Changed, FontSizeProperty.Changed,
- FontStyleProperty.Changed, FontWeightProperty.Changed,
- FontFamilyProperty.Changed, TextDecorationsProperty.Changed,
- PaddingProperty.Changed, MaxLinesProperty.Changed, LineHeightProperty.Changed
- ).AddClassHandler((x, _) => x.InvalidateTextLayout());
+
+ AffectsRender(BackgroundProperty, ForegroundProperty);
}
///
@@ -460,6 +447,7 @@ protected virtual TextLayout CreateTextLayout(Size constraint, string text)
TextWrapping,
TextTrimming,
TextDecorations,
+ FlowDirection,
constraint.Width,
constraint.Height,
maxLines: MaxLines,
@@ -472,6 +460,8 @@ protected virtual TextLayout CreateTextLayout(Size constraint, string text)
protected void InvalidateTextLayout()
{
_textLayout = null;
+
+ InvalidateMeasure();
}
///
@@ -507,12 +497,40 @@ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e
base.OnAttachedToLogicalTree(e);
InvalidateTextLayout();
-
- InvalidateMeasure();
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ switch (change.Property.Name)
+ {
+ case nameof (FontSize):
+ case nameof (FontWeight):
+ case nameof (FontStyle):
+ case nameof (FontFamily):
+
+ case nameof (TextWrapping):
+ case nameof (TextTrimming):
+ case nameof (TextAlignment):
+ case nameof (FlowDirection):
+
+ case nameof (Padding):
+ case nameof (LineHeight):
+ case nameof (MaxLines):
+
+ case nameof (Text):
+ case nameof (TextDecorations):
+ case nameof (Foreground):
+ {
+ InvalidateTextLayout();
+ break;
+ }
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 20d8a94c1a8..c8955f2e1c1 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -14,6 +14,8 @@
using Avalonia.Layout;
using Avalonia.Utilities;
using Avalonia.Controls.Metadata;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls
{
@@ -250,6 +252,7 @@ public int CaretIndex
{
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
+
UndoRedoState state;
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState();
@@ -301,7 +304,8 @@ public int SelectionStart
{
UpdateCommandStates();
}
- if (SelectionStart == SelectionEnd)
+
+ if (value == SelectionEnd)
{
CaretIndex = SelectionStart;
}
@@ -319,13 +323,15 @@ public int SelectionEnd
{
value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
+
if (changed)
{
UpdateCommandStates();
}
- if (SelectionStart == SelectionEnd)
+
+ if (value == SelectionStart)
{
- CaretIndex = SelectionEnd;
+ CaretIndex = value;
}
}
}
@@ -345,6 +351,7 @@ public string Text
if (!_ignoreTextChanges)
{
var caretIndex = CaretIndex;
+
SelectionStart = CoerceCaretIndex(SelectionStart, value);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
@@ -533,13 +540,27 @@ public event EventHandler PastingFromClipboard
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get("PART_TextPresenter");
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
_imClient.SetPresenter(_presenter, this);
+
if (IsFocused)
{
_presenter?.ShowCaret();
}
}
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+
+ _imClient.SetPresenter(null, null);
+ }
+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@@ -631,9 +652,8 @@ private void HandleTextInput(string input)
_selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false);
- string text = Text ?? string.Empty;
- int caretIndex = CaretIndex;
- int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
+ var text = Text ?? string.Empty;
+ var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
if (MaxLength > 0 && newLength > MaxLength)
{
@@ -649,11 +669,11 @@ private void HandleTextInput(string input)
try
{
DeleteSelection(false);
- caretIndex = CaretIndex;
+ var caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
- CaretIndex += input.Length;
ClearSelection();
+
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
@@ -663,6 +683,8 @@ private void HandleTextInput(string input)
{
RaisePropertyChanged(TextProperty, oldText, _text);
}
+
+ CaretIndex = caretIndex + input.Length;
}
finally
{
@@ -684,6 +706,7 @@ public string RemoveInvalidCharacters(string text)
public async void Cut()
{
var text = GetSelection();
+
if (string.IsNullOrEmpty(text))
{
return;
@@ -703,6 +726,7 @@ public async void Cut()
public async void Copy()
{
var text = GetSelection();
+
if (string.IsNullOrEmpty(text))
{
return;
@@ -739,11 +763,16 @@ public async void Paste()
protected override void OnKeyDown(KeyEventArgs e)
{
- string text = Text ?? string.Empty;
- int caretIndex = CaretIndex;
- bool movement = false;
- bool selection = false;
- bool handled = false;
+ if (_presenter == null)
+ {
+ return;
+ }
+
+ var text = Text ?? string.Empty;
+ var caretIndex = CaretIndex;
+ var movement = false;
+ var selection = false;
+ var handled = false;
var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetService();
@@ -884,46 +913,85 @@ protected override void OnKeyDown(KeyEventArgs e)
break;
case Key.Up:
- movement = MoveVertical(-1);
+ {
selection = DetectSelection();
- break;
+
+ _presenter.MoveCaretVertical(LogicalDirection.Backward);
+
+ if (caretIndex != _presenter.CaretIndex)
+ {
+ movement = true;
+ }
+ if (selection)
+ {
+ SelectionEnd = _presenter.CaretIndex;
+ }
+
+ break;
+ }
case Key.Down:
- movement = MoveVertical(1);
+ {
selection = DetectSelection();
+
+ _presenter?.MoveCaretVertical();
+
+ if (caretIndex != _presenter.CaretIndex)
+ {
+ movement = true;
+ }
+
+ if (selection)
+ {
+ SelectionEnd = _presenter.CaretIndex;
+ }
+
break;
-
+ }
case Key.Back:
+ {
SnapshotUndoRedo();
+
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
}
- if (!DeleteSelection() && CaretIndex > 0)
+ if (!DeleteSelection() && caretIndex > 0)
{
- var removedCharacters = 1;
- // handle deleting /r/n
- // you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if
- // a /r should also be deleted.
- if (CaretIndex > 1 &&
- text[CaretIndex - 1] == '\n' &&
- text[CaretIndex - 2] == '\r')
+ var removedCharacters = 0;
+
+ // \r\n needs special treatment here
+ if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r')
{
removedCharacters = 2;
}
+ else
+ {
+ Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters);
+ }
+
+ if (removedCharacters == 0)
+ {
+ return;
+ }
+
+ var length = Math.Max(0, caretIndex - removedCharacters);
- SetTextInternal(text.Substring(0, caretIndex - removedCharacters) +
+ SetTextInternal(text.Substring(0, length) +
text.Substring(caretIndex));
- CaretIndex -= removedCharacters;
+
+ CaretIndex = caretIndex - removedCharacters;
+
ClearSelection();
}
handled = true;
break;
-
+ }
case Key.Delete:
SnapshotUndoRedo();
+
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
@@ -931,21 +999,18 @@ protected override void OnKeyDown(KeyEventArgs e)
if (!DeleteSelection() && caretIndex < text.Length)
{
- var removedCharacters = 1;
- // handle deleting /r/n
- // you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if
- // a /r should also be deleted.
- if (CaretIndex < text.Length - 1 &&
- text[caretIndex + 1] == '\n' &&
- text[caretIndex] == '\r')
- {
- removedCharacters = 2;
- }
+ _presenter.MoveCaretHorizontal();
+
+ var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex);
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
+
+ CaretIndex = caretIndex;
}
+ SnapshotUndoRedo();
+
handled = true;
break;
@@ -983,11 +1048,7 @@ protected override void OnKeyDown(KeyEventArgs e)
}
}
- if (movement && selection)
- {
- SelectionEnd = CaretIndex;
- }
- else if (movement)
+ if (movement && !selection)
{
ClearSelection();
}
@@ -1000,19 +1061,28 @@ protected override void OnKeyDown(KeyEventArgs e)
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
+ if (_presenter == null)
+ {
+ return;
+ }
+
var text = Text;
-
var clickInfo = e.GetCurrentPoint(this);
- if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border))
+
+ if (text != null && clickInfo.Properties.IsLeftButtonPressed &&
+ !(clickInfo.Pointer?.Captured is Border))
{
var point = e.GetPosition(_presenter);
- var index = _presenter.GetCaretIndex(point);
- var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift);
- if (!clickToSelect)
- {
- CaretIndex = index;
- }
+ var oldIndex = CaretIndex;
+
+ _presenter.MoveCaretToPoint(point);
+
+ var index = _presenter.CaretIndex;
+
+ var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
+
+ SetAndRaise(CaretIndexProperty, ref _caretIndex, index);
#pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount)
@@ -1021,13 +1091,14 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
case 1:
if (clickToSelect)
{
- SelectionStart = Math.Min(index, CaretIndex);
- SelectionEnd = Math.Max(index, CaretIndex);
+ SelectionStart = Math.Min(oldIndex, index);
+ SelectionEnd = Math.Max(oldIndex, index);
}
else
{
SelectionStart = SelectionEnd = index;
}
+
break;
case 2:
if (!StringUtils.IsStartOfWord(text, index))
@@ -1049,8 +1120,13 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
protected override void OnPointerMoved(PointerEventArgs e)
{
+ if (_presenter == null)
+ {
+ return;
+ }
+
// selection should not change during pointer move if the user right clicks
- if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+ if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(_presenter);
@@ -1058,32 +1134,45 @@ protected override void OnPointerMoved(PointerEventArgs e)
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
- CaretIndex = SelectionEnd = _presenter.GetCaretIndex(point);
+ _presenter.MoveCaretToPoint(point);
+
+ SelectionEnd = _presenter.CaretIndex;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
- if (_presenter != null && e.Pointer.Captured == _presenter)
+ if (_presenter == null)
{
- if (e.InitialPressMouseButton == MouseButton.Right)
+ return;
+ }
+
+ if (e.Pointer.Captured != _presenter)
+ {
+ return;
+ }
+
+ if (e.InitialPressMouseButton == MouseButton.Right)
+ {
+ var point = e.GetPosition(_presenter);
+
+ _presenter.MoveCaretToPoint(point);
+
+ var caretIndex = _presenter.CaretIndex;
+
+ // see if mouse clicked inside current selection
+ // if it did not, we change the selection to where the user clicked
+ var firstSelection = Math.Min(SelectionStart, SelectionEnd);
+ var lastSelection = Math.Max(SelectionStart, SelectionEnd);
+ var didClickInSelection = SelectionStart != SelectionEnd &&
+ caretIndex >= firstSelection && caretIndex <= lastSelection;
+ if (!didClickInSelection)
{
- var point = e.GetPosition(_presenter);
- var caretIndex = _presenter.GetCaretIndex(point);
-
- // see if mouse clicked inside current selection
- // if it did not, we change the selection to where the user clicked
- var firstSelection = Math.Min(SelectionStart, SelectionEnd);
- var lastSelection = Math.Max(SelectionStart, SelectionEnd);
- var didClickInSelection = SelectionStart != SelectionEnd &&
- caretIndex >= firstSelection && caretIndex <= lastSelection;
- if (!didClickInSelection)
- {
- CaretIndex = SelectionEnd = SelectionStart = caretIndex;
- }
+ CaretIndex = SelectionEnd = SelectionStart = caretIndex;
}
- e.Pointer.Capture(null);
}
+
+ e.Pointer.Capture(null);
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value)
@@ -1127,106 +1216,46 @@ public void Clear()
Text = string.Empty;
}
- private int DeleteCharacter(int index)
- {
- var start = index + 1;
- var text = Text;
- var c = text[index];
- var result = 1;
-
- if (c == '\n' && index > 0 && text[index - 1] == '\r')
- {
- --index;
- ++result;
- }
- else if (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n')
- {
- ++start;
- ++result;
- }
-
- Text = text.Substring(0, index) + text.Substring(start);
-
- return result;
- }
-
private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
{
var text = Text ?? string.Empty;
- var caretIndex = CaretIndex;
+ var selectionStart = SelectionStart;
if (!wholeWord)
{
- if (SelectionStart != SelectionEnd && !isSelecting)
+ if (_presenter == null)
{
- var start = Math.Min(SelectionStart, SelectionEnd);
- var end = Math.Max(SelectionStart, SelectionEnd);
- CaretIndex = direction < 0 ? start : end;
return;
}
+
+ _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward);
- var index = caretIndex + direction;
-
- if (index < 0 || index > text.Length)
+ if (isSelecting)
{
- return;
- }
- else if (index == text.Length)
- {
- CaretIndex = index;
- return;
- }
-
- var c = text[index];
-
- if (direction > 0)
- {
- CaretIndex += (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n') ? 2 : 1;
+ SelectionEnd = _presenter.CaretIndex;
}
else
{
- CaretIndex -= (c == '\n' && index > 0 && text[index - 1] == '\r') ? 2 : 1;
+ SelectionStart = SelectionEnd = _presenter.CaretIndex;
}
}
else
{
if (direction > 0)
{
- CaretIndex += StringUtils.NextWord(text, caretIndex) - caretIndex;
+ var offset = StringUtils.NextWord(text, selectionStart) - selectionStart;
+
+ CaretIndex += offset;
}
else
{
- CaretIndex += StringUtils.PreviousWord(text, caretIndex) - caretIndex;
+ var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart;
+
+ CaretIndex += offset;
}
}
}
- private bool MoveVertical(int count)
- {
- if (_presenter is null)
- {
- return false;
- }
-
- var formattedText = _presenter.FormattedText;
- var lines = formattedText.GetLines().ToList();
- var caretIndex = CaretIndex;
- var lineIndex = GetLine(caretIndex, lines) + count;
-
- if (lineIndex >= 0 && lineIndex < lines.Count)
- {
- var line = lines[lineIndex];
- var rect = formattedText.HitTestTextPosition(caretIndex);
- var y = count < 0 ? rect.Y : rect.Bottom;
- var point = new Point(rect.X, y + (count * (line.Height / 2)));
- var hit = formattedText.HitTestPoint(point);
- CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
- return true;
- }
-
- return false;
- }
-
private void MoveHome(bool document)
{
if (_presenter is null)
@@ -1243,17 +1272,17 @@ private void MoveHome(bool document)
}
else
{
- var lines = _presenter.FormattedText.GetLines();
+ var lines = _presenter.TextLayout.TextLines;
var pos = 0;
foreach (var line in lines)
{
- if (pos + line.Length > caretIndex || pos + line.Length == text.Length)
+ if (pos + line.TextRange.Length > caretIndex || pos + line.TextRange.Length == text.Length)
{
break;
}
- pos += line.Length;
+ pos += line.TextRange.Length;
}
caretIndex = pos;
@@ -1278,12 +1307,12 @@ private void MoveEnd(bool document)
}
else
{
- var lines = _presenter.FormattedText.GetLines();
+ var lines = _presenter.TextLayout.TextLines;
var pos = 0;
foreach (var line in lines)
{
- pos += line.Length;
+ pos += line.TextRange.Length;
if (pos > caretIndex)
{
@@ -1360,25 +1389,6 @@ private string GetSelection()
return text.Substring(start, end - start);
}
- private int GetLine(int caretIndex, IList lines)
- {
- int pos = 0;
- int i;
-
- for (i = 0; i < lines.Count - 1; ++i)
- {
- var line = lines[i];
- pos += line.Length;
-
- if (pos > caretIndex)
- {
- break;
- }
- }
-
- return i;
- }
-
private void SetTextInternal(string value, bool raiseTextChanged = true)
{
if (raiseTextChanged)
diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
index 334db2cafd9..279efa29347 100644
--- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
+++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
@@ -10,7 +11,7 @@ internal class TextBoxTextInputMethodClient : ITextInputMethodClient
{
private InputElement _parent;
private TextPresenter _presenter;
- private IDisposable _subscription;
+
public Rect CursorRectangle
{
get
@@ -20,11 +21,15 @@ public Rect CursorRectangle
return default;
}
var transform = _presenter.TransformToVisual(_parent);
+
if (transform == null)
{
return default;
}
- return _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
+
+ var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
+
+ return rect;
}
}
@@ -40,20 +45,25 @@ public event EventHandler SurroundingTextChanged { add { } remove { } }
public string TextBeforeCursor => null;
public string TextAfterCursor => null;
- private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
+ private void OnCaretBoundsChanged(object sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
public void SetPresenter(TextPresenter presenter, InputElement parent)
{
_parent = parent;
- _subscription?.Dispose();
- _subscription = null;
+
+ if (_presenter != null)
+ {
+ _presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
+ }
+
_presenter = presenter;
+
if (_presenter != null)
{
- _subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty)
- .Subscribe(OnCaretIndexChanged);
+ _presenter.CaretBoundsChanged += OnCaretBoundsChanged;
}
+
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
}
diff --git a/src/Avalonia.Controls/Utils/StringUtils.cs b/src/Avalonia.Controls/Utils/StringUtils.cs
index 8cf2e836bb7..53937003c85 100644
--- a/src/Avalonia.Controls/Utils/StringUtils.cs
+++ b/src/Avalonia.Controls/Utils/StringUtils.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls.Utils
{
@@ -23,26 +24,38 @@ public static bool IsStartOfWord(string text, int index)
return false;
}
+ var codepoint = new Codepoint(text[index]);
+
// A 'word' starts with an AlphaNumeric or some punctuation symbols immediately
// preceeded by lwsp.
- if (index > 0 && !char.IsWhiteSpace(text[index - 1]))
+ if (index > 0)
{
- return false;
+ var previousCodepoint = new Codepoint(text[index - 1]);
+
+ if (!previousCodepoint.IsWhiteSpace)
+ {
+ return false;
+ }
+
+ if (previousCodepoint.IsBreakChar)
+ {
+ return true;
+ }
}
- switch (CharUnicodeInfo.GetUnicodeCategory(text[index]))
+ switch (codepoint.GeneralCategory)
{
- case UnicodeCategory.LowercaseLetter:
- case UnicodeCategory.TitlecaseLetter:
- case UnicodeCategory.UppercaseLetter:
- case UnicodeCategory.DecimalDigitNumber:
- case UnicodeCategory.LetterNumber:
- case UnicodeCategory.OtherNumber:
- case UnicodeCategory.DashPunctuation:
- case UnicodeCategory.InitialQuotePunctuation:
- case UnicodeCategory.OpenPunctuation:
- case UnicodeCategory.CurrencySymbol:
- case UnicodeCategory.MathSymbol:
+ case GeneralCategory.LowercaseLetter:
+ case GeneralCategory.TitlecaseLetter:
+ case GeneralCategory.UppercaseLetter:
+ case GeneralCategory.DecimalNumber:
+ case GeneralCategory.LetterNumber:
+ case GeneralCategory.OtherNumber:
+ case GeneralCategory.DashPunctuation:
+ case GeneralCategory.InitialPunctuation:
+ case GeneralCategory.OpenPunctuation:
+ case GeneralCategory.CurrencySymbol:
+ case GeneralCategory.MathSymbol:
return true;
// TODO: How do you do this in .NET?
@@ -56,6 +69,11 @@ public static bool IsStartOfWord(string text, int index)
public static int PreviousWord(string text, int cursor)
{
+ if (string.IsNullOrEmpty(text))
+ {
+ return 0;
+ }
+
int begin;
int i;
int cr;
@@ -107,7 +125,12 @@ public static int NextWord(string text, int cursor)
cr = LineEnd(text, cursor);
- if (cr < text.Length && text[cr] == '\r' && text[cr + 1] == '\n')
+ if (cursor >= text.Length)
+ {
+ return cursor;
+ }
+
+ if (cr < text.Length && text[cr] == '\r' && cr + 1 < text.Length && text[cr + 1] == '\n')
{
lf = cr + 1;
}
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 48d0ef9da9f..90221bb9223 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -27,11 +27,6 @@ public static void Initialize()
public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
- public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans)
- {
- return new HeadlessFormattedTextStub(text, constraint);
- }
-
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect);
public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
@@ -354,11 +349,6 @@ public void Clear(Color color)
}
- public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
- {
-
- }
-
public IDrawingContextLayerImpl CreateLayer(Size size)
{
return new HeadlessBitmapStub(size, new Vector(96, 96));
@@ -474,31 +464,5 @@ public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrush
return new HeadlessDrawingContextStub();
}
}
-
- class HeadlessFormattedTextStub : IFormattedTextImpl
- {
- public HeadlessFormattedTextStub(string text, Size constraint)
- {
- Text = text;
- Constraint = constraint;
- Bounds = new Rect(Constraint.Constrain(new Size(50, 50)));
- }
-
- public Size Constraint { get; }
- public Rect Bounds { get; }
- public string Text { get; }
-
-
- public IEnumerable GetLines()
- {
- return new[] { new FormattedTextLine(Text.Length, 10) };
- }
-
- public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
-
- public Rect HitTestTextPosition(int index) => new Rect();
-
- public IEnumerable HitTestTextRange(int index, int length) => new Rect[length];
- }
}
}
diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
index 605659d4643..b619b9d129c 100644
--- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -133,14 +133,10 @@ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
class HeadlessTextShaperStub : ITextShaperImpl
{
- public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
- {
- return new GlyphRun(new GlyphTypeface(typeface), 10,
- new ReadOnlySlice(new ushort[] { 1, 2, 3 }),
- new ReadOnlySlice(new double[] { 1, 2, 3 }),
- new ReadOnlySlice(new Vector[] { new Vector(1, 1), new Vector(2, 2), new Vector(3, 3) }),
- text,
- new ReadOnlySlice(new ushort[] { 1, 2, 3 }));
+ public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
+ CultureInfo culture, sbyte bidiLevel)
+ {
+ return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
}
}
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index 68e3673cfef..828ea1f1847 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -5,11 +5,50 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Media.FormattedText)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor(Avalonia.Platform.IPlatformRenderInterface)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.FormattedText.Bounds.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.FormattedText.Constraint.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Constraint.set(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Double Avalonia.Media.FormattedText.FontSize.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText.FontSize.set(System.Double)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Collections.Generic.IEnumerable Avalonia.Media.FormattedText.GetLines()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextHitTestResult Avalonia.Media.FormattedText.HitTestPoint(Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.FormattedText.HitTestTextPosition(System.Int32)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Collections.Generic.IEnumerable Avalonia.Media.FormattedText.HitTestTextRange(System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Platform.IFormattedTextImpl Avalonia.Media.FormattedText.PlatformImpl.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.FormattedText.Spans.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Spans.set(System.Collections.Generic.IReadOnlyList)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.String Avalonia.Media.FormattedText.Text.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Text.set(System.String)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.FormattedText.TextWrapping.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText.TextWrapping.set(Avalonia.Media.TextWrapping)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FormattedText.Typeface.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Typeface.set(Avalonia.Media.Typeface)' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Media.FormattedTextLine' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Media.FormattedTextStyleSpan' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor(Avalonia.Media.GlyphTypeface, System.Double, Avalonia.Utilities.ReadOnlySlice, Avalonia.Utilities.ReadOnlySlice, Avalonia.Utilities.ReadOnlySlice, Avalonia.Utilities.ReadOnlySlice, Avalonia.Utilities.ReadOnlySlice, System.Int32)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice Avalonia.Media.GlyphRun.GlyphAdvances.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphAdvances.set(Avalonia.Utilities.ReadOnlySlice)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice Avalonia.Media.GlyphRun.GlyphClusters.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphClusters.set(Avalonia.Utilities.ReadOnlySlice)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice Avalonia.Media.GlyphRun.GlyphIndices.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphIndices.set(Avalonia.Utilities.ReadOnlySlice)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice Avalonia.Media.GlyphRun.GlyphOffsets.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphOffsets.set(Avalonia.Utilities.ReadOnlySlice)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract.
CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract.
+CannotSealType : Type 'Avalonia.Media.TextHitTestResult' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
+TypeCannotChangeClassification : Type 'Avalonia.Media.TextHitTestResult' is a 'struct' in the implementation but is a 'class' in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult..ctor()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsInside.set(System.Boolean)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsTrailing.set(System.Boolean)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.TextPosition.set(System.Int32)' does not exist in the implementation but it does exist in the contract.
TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
@@ -23,8 +62,14 @@ CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFor
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment.get()' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping.get()' is non-virtual in the implementation but is virtual in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.GenericTextRunProperties..ctor(Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextDecorationCollection, Avalonia.Media.IBrush, Avalonia.Media.IBrush, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapeableTextCharacters..ctor(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters..ctor(Avalonia.Media.GlyphRun, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult Avalonia.Media.TextFormatting.ShapedTextCharacters.Split(System.Int32)' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'protected System.Boolean Avalonia.Media.TextFormatting.TextCharacters.TryGetRunProperties(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, Avalonia.Media.Typeface, System.Int32)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.IBrush, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Media.TextTrimming, Avalonia.Media.TextDecorationCollection, System.Double, System.Double, System.Double, System.Int32, System.Collections.Generic.IReadOnlyList>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
@@ -53,6 +98,7 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineBreak..ctor(System.Collections.Generic.IReadOnlyList)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineMetrics..ctor(Avalonia.Size, System.Double, Avalonia.Media.TextFormatting.TextRange, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLineMetrics.Create(System.Collections.Generic.IEnumerable, Avalonia.Media.TextFormatting.TextRange, System.Double, Avalonia.Media.TextFormatting.TextParagraphProperties)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLineMetrics.Size.get()' does not exist in the implementation but it does exist in the contract.
@@ -65,15 +111,23 @@ CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Media.TextFormatting.TextShaper.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Media.TextFormatting.Unicode.BiDiClass' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.Unicode.BiDiClass Avalonia.Media.TextFormatting.Unicode.Codepoint.BiDiClass.get()' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawEllipse(Avalonia.Media.IBrush, Avalonia.Media.IPen, Avalonia.Rect)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Platform.IFormattedTextImpl)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Platform.IFormattedTextImpl)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract.
+TypesMustExist : Type 'Avalonia.Platform.IFormattedTextImpl' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IFormattedTextImpl Avalonia.Platform.IPlatformRenderInterface.CreateFormattedText(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Size, System.Collections.Generic.IReadOnlyList)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public Avalonia.Platform.IFormattedTextImpl Avalonia.Platform.IPlatformRenderInterface.CreateFormattedText(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Size, System.Collections.Generic.IReadOnlyList)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
@@ -86,4 +140,9 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avaloni
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract.
-Total Issues: 87
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFormatting.ShapedBuffer Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.GlyphTypeface, System.Double, System.Globalization.CultureInfo, System.SByte)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 146
diff --git a/src/Avalonia.Visuals/Assets/BiDi.trie b/src/Avalonia.Visuals/Assets/BiDi.trie
new file mode 100644
index 00000000000..1c6122e2f1f
Binary files /dev/null and b/src/Avalonia.Visuals/Assets/BiDi.trie differ
diff --git a/src/Avalonia.Visuals/Assets/UnicodeData.trie b/src/Avalonia.Visuals/Assets/UnicodeData.trie
index f96106a5fad..46175ea6446 100644
Binary files a/src/Avalonia.Visuals/Assets/UnicodeData.trie and b/src/Avalonia.Visuals/Assets/UnicodeData.trie differ
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index 5584e526559..d0a73ca9a82 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -226,17 +226,13 @@ public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX,
///
/// Draws text.
///
- /// The foreground brush.
/// The upper-left corner of the text.
/// The text.
- public void DrawText(IBrush foreground, Point origin, FormattedText text)
+ public void DrawText(FormattedText text, Point origin)
{
_ = text ?? throw new ArgumentNullException(nameof(text));
-
- if (foreground != null)
- {
- PlatformImpl.DrawText(foreground, origin, text.PlatformImpl);
- }
+
+ text.Draw(this, origin);
}
///
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index f6129eaf6a9..12c40e4d590 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -1,214 +1,1415 @@
using System;
-using System.Collections.Generic;
-using Avalonia.Platform;
+using System.Collections;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
namespace Avalonia.Media
{
///
- /// Represents a piece of text with formatting.
+ /// The FormattedText class is targeted at programmers needing to add some simple text to a MIL visual.
///
public class FormattedText
{
- private readonly IPlatformRenderInterface _platform;
- private Size _constraint = Size.Infinity;
- private IFormattedTextImpl? _platformImpl;
- private IReadOnlyList? _spans;
- private Typeface _typeface;
- private double _fontSize;
- private string? _text;
- private TextAlignment _textAlignment;
- private TextWrapping _textWrapping;
+ public const double DefaultRealToIdeal = 28800.0 / 96;
+ public const double DefaultIdealToReal = 1 / DefaultRealToIdeal;
+ public const int IdealInfiniteWidth = 0x3FFFFFFE;
+ public const double RealInfiniteWidth = IdealInfiniteWidth * DefaultIdealToReal;
+
+ public const double GreatestMultiplierOfEm = 100;
+
+ private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm;
+
+ // properties and format runs
+ private ReadOnlySlice _text;
+ private readonly SpanVector _formatRuns = new SpanVector(null);
+ private SpanPosition _latestPosition;
+
+ private GenericTextParagraphProperties _defaultParaProps;
+
+ private double _maxTextWidth = double.PositiveInfinity;
+ private double[]? _maxTextWidths;
+ private double _maxTextHeight = double.PositiveInfinity;
+ private int _maxLineCount = int.MaxValue;
+ private TextTrimming _trimming = TextTrimming.WordEllipsis;
+
+ // text source callbacks
+ private TextSourceImplementation? _textSourceImpl;
+
+ // cached metrics
+ private CachedMetrics? _metrics;
///
- /// Initializes a new instance of the class.
+ /// Construct a FormattedText object.
///
- public FormattedText()
+ /// String of text to be displayed.
+ /// Culture of text.
+ /// Flow direction of text.
+ /// Type face used to display text.
+ /// Font em size in visual units (1/96 of an inch).
+ /// Foreground brush used to render text.
+ public FormattedText(
+ string textToFormat,
+ CultureInfo culture,
+ FlowDirection flowDirection,
+ Typeface typeface,
+ double emSize,
+ IBrush foreground)
+ {
+ if (culture is null)
+ {
+ throw new ArgumentNullException(nameof(culture));
+ }
+
+ ValidateFlowDirection(flowDirection, nameof(flowDirection));
+
+ ValidateFontSize(emSize);
+
+ _text = textToFormat != null ?
+ new ReadOnlySlice(textToFormat.AsMemory()) :
+ throw new ArgumentNullException(nameof(textToFormat));
+
+ var runProps = new GenericTextRunProperties(
+ typeface,
+ emSize,
+ null, // decorations
+ foreground,
+ null, // highlight background
+ BaselineAlignment.Baseline,
+ culture
+ );
+
+ _latestPosition = _formatRuns.SetValue(0, _text.Length, runProps, _latestPosition);
+
+ _defaultParaProps = new GenericTextParagraphProperties(
+ flowDirection,
+ TextAlignment.Left,
+ false,
+ false,
+ runProps,
+ TextWrapping.WrapWithOverflow,
+ 0, // line height not specified
+ 0 // indentation not specified
+ );
+
+ InvalidateMetrics();
+ }
+
+ private static void ValidateFontSize(double emSize)
+ {
+ if (emSize <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(emSize), "The parameter value must be greater than zero.");
+ }
+
+ if (emSize > MaxFontEmSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(emSize), $"The parameter value cannot be greater than '{MaxFontEmSize}'");
+ }
+
+ if (double.IsNaN(emSize))
+ {
+ throw new ArgumentOutOfRangeException(nameof(emSize), "The parameter value must be a number.");
+ }
+ }
+
+ private static void ValidateFlowDirection(FlowDirection flowDirection, string parameterName)
+ {
+ if ((int)flowDirection < 0 || (int)flowDirection > (int)FlowDirection.RightToLeft)
+ {
+ throw new InvalidEnumArgumentException(parameterName, (int)flowDirection, typeof(FlowDirection));
+ }
+ }
+
+ private int ValidateRange(int startIndex, int count)
+ {
+ if (startIndex < 0 || startIndex > _text.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(startIndex));
+ }
+
+ var limit = startIndex + count;
+
+ if (count < 0 || limit < startIndex || limit > _text.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ return limit;
+ }
+
+ private void InvalidateMetrics()
{
- _platform = AvaloniaLocator.Current.GetRequiredService();
+ _metrics = null;
}
///
- /// Initializes a new instance of the class.
+ /// Sets foreground brush used for drawing text
///
- /// The platform render interface.
- public FormattedText(IPlatformRenderInterface platform)
+ /// Foreground brush
+ public void SetForegroundBrush(IBrush foregroundBrush)
{
- _platform = platform;
+ SetForegroundBrush(foregroundBrush, 0, _text.Length);
}
///
- /// Initializes a new instance of the class.
+ /// Sets foreground brush used for drawing text
///
- ///
- ///
- ///
- ///
- ///
- ///
- public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
- TextWrapping textWrapping, Size constraint) : this()
+ /// Foreground brush
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetForegroundBrush(IBrush foregroundBrush, int startIndex, int count)
{
- _text = text;
+ var limit = ValidateRange(startIndex, count);
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+ i = Math.Min(limit, i + formatRider.Length);
- _typeface = typeface;
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
- _fontSize = fontSize;
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
- _textAlignment = textAlignment;
+ if (runProps.ForegroundBrush == foregroundBrush)
+ {
+ continue;
+ }
- _textWrapping = textWrapping;
+ var newProps = new GenericTextRunProperties(
+ runProps.Typeface,
+ runProps.FontRenderingEmSize,
+ runProps.TextDecorations,
+ foregroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ runProps.CultureInfo
+ );
- _constraint = constraint;
+#pragma warning restore 6506
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
+ newProps, formatRider.SpanPosition);
+ }
}
///
- /// Gets the bounds of the text within the .
+ /// Sets or changes the font family for the text object
///
- /// The bounds of the text.
- public Rect Bounds => PlatformImpl.Bounds;
+ /// Font family name
+ public void SetFontFamily(string fontFamily)
+ {
+ SetFontFamily(fontFamily, 0, _text.Length);
+ }
///
- /// Gets or sets the constraint of the text.
+ /// Sets or changes the font family for the text object
///
- public Size Constraint
+ /// Font family name
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetFontFamily(string fontFamily, int startIndex, int count)
{
- get => _constraint;
- set => Set(ref _constraint, value);
+ if (fontFamily == null)
+ {
+ throw new ArgumentNullException(nameof(fontFamily));
+ }
+
+ SetFontFamily(new FontFamily(fontFamily), startIndex, count);
}
///
- /// Gets or sets the base typeface.
+ /// Sets or changes the font family for the text object
///
- public Typeface Typeface
+ /// Font family
+ public void SetFontFamily(FontFamily fontFamily)
{
- get => _typeface;
- set => Set(ref _typeface, value);
+ SetFontFamily(fontFamily, 0, _text.Length);
+ }
+
+ ///
+ /// Sets or changes the font family for the text object
+ ///
+ /// Font family
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetFontFamily(FontFamily fontFamily, int startIndex, int count)
+ {
+ if (fontFamily == null)
+ {
+ throw new ArgumentNullException(nameof(fontFamily));
+ }
+
+ var limit = ValidateRange(startIndex, count);
+
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+
+ i = Math.Min(limit, i + formatRider.Length);
+
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
+
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
+
+ var oldTypeface = runProps.Typeface;
+
+ if (fontFamily.Equals(oldTypeface.FontFamily))
+ {
+ continue;
+ }
+
+ var newProps = new GenericTextRunProperties(
+ new Typeface(fontFamily, oldTypeface.Style, oldTypeface.Weight),
+ runProps.FontRenderingEmSize,
+ runProps.TextDecorations,
+ runProps.ForegroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ runProps.CultureInfo
+ );
+
+#pragma warning restore 6506
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
+ newProps, formatRider.SpanPosition);
+
+ InvalidateMetrics();
+ }
}
///
- /// Gets or sets the font size.
+ /// Sets or changes the font em size measured in MIL units
///
- public double FontSize
+ /// Font em size
+ public void SetFontSize(double emSize)
{
- get => _fontSize;
- set => Set(ref _fontSize, value);
+ SetFontSize(emSize, 0, _text.Length);
}
///
- /// Gets or sets a collection of spans that describe the formatting of subsections of the
- /// text.
+ /// Sets or changes the font em size measured in MIL units
///
- public IReadOnlyList? Spans
+ /// Font em size
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetFontSize(double emSize, int startIndex, int count)
{
- get => _spans;
- set => Set(ref _spans, value);
+ ValidateFontSize(emSize);
+
+ var limit = ValidateRange(startIndex, count);
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+
+ i = Math.Min(limit, i + formatRider.Length);
+
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
+
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
+
+ if (runProps.FontRenderingEmSize == emSize)
+ {
+ continue;
+ }
+
+ var newProps = new GenericTextRunProperties(
+ runProps.Typeface,
+ emSize,
+ runProps.TextDecorations,
+ runProps.ForegroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ runProps.CultureInfo
+ );
+
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
+ newProps, formatRider.SpanPosition);
+
+#pragma warning restore 6506
+ InvalidateMetrics();
+ }
}
///
- /// Gets or sets the text.
+ /// Sets or changes the culture for the text object.
///
- public string? Text
+ /// The new culture for the text object.
+ public void SetCulture(CultureInfo culture)
{
- get => _text;
- set => Set(ref _text, value);
+ SetCulture(culture, 0, _text.Length);
}
///
- /// Gets or sets the alignment of the text.
+ /// Sets or changes the culture for the text object.
+ ///
+ /// The new culture for the text object.
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetCulture(CultureInfo culture, int startIndex, int count)
+ {
+ if (culture is null)
+ {
+ throw new ArgumentNullException(nameof(culture));
+ }
+
+ var limit = ValidateRange(startIndex, count);
+
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+
+ i = Math.Min(limit, i + formatRider.Length);
+
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
+
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
+
+ if (runProps.CultureInfo == culture)
+ {
+ continue;
+ }
+
+ var newProps = new GenericTextRunProperties(
+ runProps.Typeface,
+ runProps.FontRenderingEmSize,
+ runProps.TextDecorations,
+ runProps.ForegroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ culture
+ );
+
+#pragma warning restore 6506
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
+ newProps, formatRider.SpanPosition);
+
+ InvalidateMetrics();
+ }
+ }
+
+ ///
+ /// Sets or changes the font weight
+ ///
+ /// Font weight
+ public void SetFontWeight(FontWeight weight)
+ {
+ SetFontWeight(weight, 0, _text.Length);
+ }
+
+ ///
+ /// Sets or changes the font weight
+ ///
+ /// Font weight
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetFontWeight(FontWeight weight, int startIndex, int count)
+ {
+ var limit = ValidateRange(startIndex, count);
+
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+
+ i = Math.Min(limit, i + formatRider.Length);
+
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
+
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
+
+ var oldTypeface = runProps.Typeface;
+
+ if (oldTypeface.Weight == weight)
+ {
+ continue;
+ }
+
+ var newProps = new GenericTextRunProperties(
+ new Typeface(oldTypeface.FontFamily, oldTypeface.Style, weight),
+ runProps.FontRenderingEmSize,
+ runProps.TextDecorations,
+ runProps.ForegroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ runProps.CultureInfo
+ );
+#pragma warning restore 6506
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
+
+ InvalidateMetrics();
+ }
+ }
+
+ ///
+ /// Sets or changes the font style
+ ///
+ /// Font style
+ public void SetFontStyle(FontStyle style)
+ {
+ SetFontStyle(style, 0, _text.Length);
+ }
+
+ ///
+ /// Sets or changes the font style
+ ///
+ /// Font style
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetFontStyle(FontStyle style, int startIndex, int count)
+ {
+ var limit = ValidateRange(startIndex, count);
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+
+ i = Math.Min(limit, i + formatRider.Length);
+
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
+
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
+
+ var oldTypeface = runProps.Typeface;
+
+ if (oldTypeface.Style == style)
+ {
+ continue;
+ }
+
+ var newProps = new GenericTextRunProperties(
+ new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight),
+ runProps.FontRenderingEmSize,
+ runProps.TextDecorations,
+ runProps.ForegroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ runProps.CultureInfo
+ );
+#pragma warning restore 6506
+
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
+
+ InvalidateMetrics(); // invalidate cached metrics
+ }
+ }
+
+ ///
+ /// Sets or changes the type face
+ ///
+ /// Typeface
+ public void SetFontTypeface(Typeface typeface)
+ {
+ SetFontTypeface(typeface, 0, _text.Length);
+ }
+
+ ///
+ /// Sets or changes the type face
+ ///
+ /// Typeface
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetFontTypeface(Typeface typeface, int startIndex, int count)
+ {
+ var limit = ValidateRange(startIndex, count);
+
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+
+ i = Math.Min(limit, i + formatRider.Length);
+
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
+
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
+
+ if (runProps.Typeface == typeface)
+ {
+ continue;
+ }
+
+ var newProps = new GenericTextRunProperties(
+ typeface,
+ runProps.FontRenderingEmSize,
+ runProps.TextDecorations,
+ runProps.ForegroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ runProps.CultureInfo
+ );
+#pragma warning restore 6506
+
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
+ newProps, formatRider.SpanPosition);
+
+ InvalidateMetrics();
+ }
+ }
+
+ ///
+ /// Sets or changes the text decorations
+ ///
+ /// Text decorations
+ public void SetTextDecorations(TextDecorationCollection textDecorations)
+ {
+ SetTextDecorations(textDecorations, 0, _text.Length);
+ }
+
+ ///
+ /// Sets or changes the text decorations
+ ///
+ /// Text decorations
+ /// The start index of initial character to apply the change to.
+ /// The number of characters the change should be applied to.
+ public void SetTextDecorations(TextDecorationCollection textDecorations, int startIndex, int count)
+ {
+ var limit = ValidateRange(startIndex, count);
+
+ for (var i = startIndex; i < limit;)
+ {
+ var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
+
+ i = Math.Min(limit, i + formatRider.Length);
+
+#pragma warning disable 6506
+ // Presharp warns that runProps is not validated, but it can never be null
+ // because the rider is already checked to be in range
+
+ if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
+ {
+ throw new NotSupportedException($"{nameof(runProps)} can not be null.");
+ }
+
+ if (runProps.TextDecorations == textDecorations)
+ {
+ continue;
+ }
+
+ var newProps = new GenericTextRunProperties(
+ runProps.Typeface,
+ runProps.FontRenderingEmSize,
+ textDecorations,
+ runProps.ForegroundBrush,
+ runProps.BackgroundBrush,
+ runProps.BaselineAlignment,
+ runProps.CultureInfo
+ );
+#pragma warning restore 6506
+
+ _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
+ newProps, formatRider.SpanPosition);
+ }
+ }
+
+ /// Note: enumeration is temporarily made private
+ /// because of PS #828532
+ ///
+ ///
+ /// Strongly typed enumerator used for enumerating text lines
+ ///
+ private struct LineEnumerator : IEnumerator, IDisposable
+ {
+ private int _lineCount;
+ private double _totalHeight;
+ private TextLine? _nextLine;
+ private readonly TextFormatter _formatter;
+ private readonly FormattedText _that;
+ private readonly ITextSource _textSource;
+
+ // these are needed because _currentLine can be disposed before the next MoveNext() call
+ private double _previousHeight;
+
+ // line break before _currentLine, needed in case we have to reformat it with collapsing symbol
+ private TextLineBreak? _previousLineBreak;
+
+ internal LineEnumerator(FormattedText text)
+ {
+ _previousHeight = 0;
+ Length = 0;
+ _previousLineBreak = null;
+
+ Position = 0;
+ _lineCount = 0;
+ _totalHeight = 0;
+ Current = null;
+ _nextLine = null;
+ _formatter = TextFormatter.Current;
+ _that = text;
+ _textSource = _that._textSourceImpl ??= new TextSourceImplementation(_that);
+ }
+
+ public void Dispose()
+ {
+ Current = null;
+
+ _nextLine = null;
+ }
+
+ private int Position { get; set; }
+
+ private int Length { get; set; }
+
+ ///
+ /// Gets the current text line in the collection
+ ///
+ public TextLine? Current { get; private set; }
+
+ ///
+ /// Gets the current text line in the collection
+ ///
+ object? IEnumerator.Current => Current;
+
+ ///
+ /// Gets the paragraph width used to format the current text line
+ ///
+ internal double CurrentParagraphWidth
+ {
+ get
+ {
+ return MaxLineLength(_lineCount);
+ }
+ }
+
+ private double MaxLineLength(int line)
+ {
+ if (_that._maxTextWidths == null)
+ return _that._maxTextWidth;
+ return _that._maxTextWidths[Math.Min(line, _that._maxTextWidths.Length - 1)];
+ }
+
+ ///
+ /// Advances the enumerator to the next text line of the collection
+ ///
+ /// true if the enumerator was successfully advanced to the next element;
+ /// false if the enumerator has passed the end of the collection
+ public bool MoveNext()
+ {
+ if (Current == null)
+ { // this is the first line
+ if (_that._text.Length == 0)
+ {
+ return false;
+ }
+
+ Current = FormatLine(
+ _textSource,
+ Position,
+ MaxLineLength(_lineCount),
+ _that._defaultParaProps!,
+ null // no previous line break
+ );
+
+ // check if this line fits the text height
+ if (_totalHeight + Current.Height > _that._maxTextHeight)
+ {
+ Current = null;
+
+ return false;
+ }
+ Debug.Assert(_nextLine == null);
+ }
+ else
+ {
+ // there is no next line or it didn't fit
+ // either way we're finished
+ if (_nextLine == null)
+ {
+ return false;
+ }
+
+ _totalHeight += _previousHeight;
+ Position += Length;
+ ++_lineCount;
+
+ Current = _nextLine;
+ _nextLine = null;
+ }
+
+ var currentLineBreak = Current.TextLineBreak;
+
+ // this line is guaranteed to fit the text height
+ Debug.Assert(_totalHeight + Current.Height <= _that._maxTextHeight);
+
+ // now, check if the next line fits, we need to do this on this iteration
+ // because we might need to add ellipsis to the current line
+ // as a result of the next line measurement
+
+ // maybe there is no next line at all
+ if (Position + Current.TextRange.Length < _that._text.Length)
+ {
+ bool nextLineFits;
+
+ if (_lineCount + 1 >= _that._maxLineCount)
+ {
+ nextLineFits = false;
+ }
+ else
+ {
+ _nextLine = FormatLine(
+ _textSource,
+ Position + Current.TextRange.Length,
+ MaxLineLength(_lineCount + 1),
+ _that._defaultParaProps,
+ currentLineBreak
+ );
+
+ nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
+ }
+
+ if (!nextLineFits)
+ {
+ _nextLine = null;
+
+ if (_that._trimming != TextTrimming.None && !Current.HasCollapsed)
+ {
+ // recreate the current line with ellipsis added
+ // Note: Paragraph ellipsis is not supported today. We'll workaround
+ // it here by faking a non-wrap text on finite column width.
+ var currentWrap = _that._defaultParaProps!.TextWrapping;
+
+ _that._defaultParaProps.SetTextWrapping(TextWrapping.NoWrap);
+
+ Current = FormatLine(
+ _that._textSourceImpl!,
+ Position,
+ MaxLineLength(_lineCount),
+ _that._defaultParaProps,
+ _previousLineBreak
+ );
+
+ currentLineBreak = Current.TextLineBreak;
+
+ _that._defaultParaProps.SetTextWrapping(currentWrap);
+ }
+ }
+ }
+
+ _previousHeight = Current.Height;
+
+ Length = Current.TextRange.Length;
+
+ _previousLineBreak = currentLineBreak;
+
+ return true;
+ }
+
+ ///
+ /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
+ ///
+ private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
+ {
+ var line = _formatter.FormatLine(
+ textSource,
+ textSourcePosition,
+ maxLineLength,
+ paraProps,
+ lineBreak
+ );
+
+ if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.TextRange.Length > 0)
+ {
+ // what I really need here is the last displayed text run of the line
+ // textSourcePosition + line.Length - 1 works except the end of paragraph case,
+ // where line length includes the fake paragraph break run
+ Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.TextRange.Length <= _that._text.Length + 1);
+
+ var thatFormatRider = new SpanRider(
+ _that._formatRuns,
+ _that._latestPosition,
+ Math.Min(textSourcePosition + line.TextRange.Length - 1, _that._text.Length - 1)
+ );
+
+ var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
+
+ TextCollapsingProperties trailingEllipsis;
+
+ if (_that._trimming == TextTrimming.CharacterEllipsis)
+ {
+ trailingEllipsis = new TextTrailingCharacterEllipsis(maxLineLength, lastRunProps);
+ }
+ else
+ {
+ Debug.Assert(_that._trimming == TextTrimming.WordEllipsis);
+ trailingEllipsis = new TextTrailingWordEllipsis(maxLineLength, lastRunProps);
+ }
+
+ var collapsedLine = line.Collapse(trailingEllipsis);
+
+ line = collapsedLine;
+ }
+ return line;
+ }
+
+
+ ///
+ /// Sets the enumerator to its initial position,
+ /// which is before the first element in the collection
+ ///
+ public void Reset()
+ {
+ Position = 0;
+ _lineCount = 0;
+ _totalHeight = 0;
+ Current = null;
+ _nextLine = null;
+ }
+ }
+
+ ///
+ /// Returns an enumerator that can iterate through the text line collection
+ ///
+ private LineEnumerator GetEnumerator()
+ {
+ return new LineEnumerator(this);
+ }
+#if NEVER
+ ///
+ /// Returns an enumerator that can iterate through the text line collection
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+#endif
+
+ private void AdvanceLineOrigin(ref Point lineOrigin, TextLine currentLine)
+ {
+ var height = currentLine.Height;
+
+ // advance line origin according to the flow direction
+ switch (_defaultParaProps.FlowDirection)
+ {
+ case FlowDirection.LeftToRight:
+ case FlowDirection.RightToLeft:
+ lineOrigin = lineOrigin.WithY(lineOrigin.Y + height);
+ break;
+ }
+ }
+
+ private class CachedMetrics
+ {
+ // vertical
+ public double Height;
+ public double Baseline;
+
+ // horizontal
+ public double Width;
+ public double WidthIncludingTrailingWhitespace;
+
+ // vertical bounding box metrics
+ public double Extent;
+ public double OverhangAfter;
+
+ // horizontal bounding box metrics
+ public double OverhangLeading;
+ public double OverhangTrailing;
+ }
+
+ ///
+ /// Defines the flow direction
+ ///
+ public FlowDirection FlowDirection
+ {
+ set
+ {
+ ValidateFlowDirection(value, "value");
+ _defaultParaProps.SetFlowDirection(value);
+ InvalidateMetrics();
+ }
+ get
+ {
+ return _defaultParaProps.FlowDirection;
+ }
+ }
+
+ ///
+ /// Defines the alignment of text within the column
///
public TextAlignment TextAlignment
{
- get => _textAlignment;
- set => Set(ref _textAlignment, value);
+ set
+ {
+ _defaultParaProps.SetTextAlignment(value);
+ InvalidateMetrics();
+ }
+ get
+ {
+ return _defaultParaProps.TextAlignment;
+ }
+ }
+
+ ///
+ /// Gets or sets the height of, or the spacing between, each line where
+ /// zero represents the default line height.
+ ///
+ public double LineHeight
+ {
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "Parameter must be greater than or equal to zero.");
+ }
+
+ _defaultParaProps.SetLineHeight(value);
+
+ InvalidateMetrics();
+ }
+ get
+ {
+ return _defaultParaProps.LineHeight;
+ }
+ }
+
+ ///
+ /// The MaxTextWidth property defines the alignment edges for the FormattedText.
+ /// For example, left aligned text is wrapped such that the leftmost glyph alignment point
+ /// on each line falls exactly on the left edge of the rectangle.
+ /// Note that for many fonts, especially in italic style, some glyph strokes may extend beyond the edges of the alignment rectangle.
+ /// For this reason, it is recommended that clients draw text with at least 1/6 em (i.e of the font size) unused margin space either side.
+ /// Zero value of MaxTextWidth is equivalent to the maximum possible paragraph width.
+ ///
+ public double MaxTextWidth
+ {
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "Parameter must be greater than or equal to zero.");
+ }
+
+ _maxTextWidth = value;
+
+ InvalidateMetrics();
+ }
+ get
+ {
+ return _maxTextWidth;
+ }
+ }
+
+ ///
+ /// Sets the array of lengths,
+ /// which will be applied to each line of text in turn.
+ /// If the text covers more lines than there are entries in the length array,
+ /// the last entry is reused as many times as required.
+ /// The maxTextWidths array overrides the MaxTextWidth property.
+ ///
+ /// The max text width array
+ public void SetMaxTextWidths(double[] maxTextWidths)
+ {
+ if (maxTextWidths == null || maxTextWidths.Length <= 0)
+ {
+ throw new ArgumentNullException(nameof(maxTextWidths));
+ }
+
+ _maxTextWidths = maxTextWidths;
+
+ InvalidateMetrics();
+ }
+
+ ///
+ /// Obtains a copy of the array of lengths,
+ /// which will be applied to each line of text in turn.
+ /// If the text covers more lines than there are entries in the length array,
+ /// the last entry is reused as many times as required.
+ /// The maxTextWidths array overrides the MaxTextWidth property.
+ ///
+ /// The copy of max text width array
+ public double[] GetMaxTextWidths()
+ {
+ return _maxTextWidths != null ? (double[])_maxTextWidths.Clone() : Array.Empty();
+ }
+
+ ///
+ /// Sets the maximum length of a column of text.
+ /// The last line of text displayed is the last whole line that will fit within this limit,
+ /// or the nth line as specified by MaxLineCount, whichever occurs first.
+ /// Use the Trimming property to control how the omission of text is indicated.
+ ///
+ public double MaxTextHeight
+ {
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"'{nameof(MaxTextHeight)}' property value must be greater than zero.");
+ }
+
+ if (double.IsNaN(value))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"'{nameof(MaxTextHeight)}' property value cannot be NaN.");
+ }
+
+ _maxTextHeight = value;
+
+ InvalidateMetrics();
+ }
+ get
+ {
+ return _maxTextHeight;
+ }
+ }
+
+ ///
+ /// Defines the maximum number of lines to display.
+ /// The last line of text displayed is the lineCount-1'th line,
+ /// or the last whole line that will fit within the count set by MaxTextHeight,
+ /// whichever occurs first.
+ /// Use the Trimming property to control how the omission of text is indicated
+ ///
+ public int MaxLineCount
+ {
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "The parameter value must be greater than zero.");
+ }
+
+ _maxLineCount = value;
+
+ InvalidateMetrics();
+ }
+ get
+ {
+ return _maxLineCount;
+ }
+ }
+
+ ///
+ /// Defines how omission of text is indicated.
+ /// CharacterEllipsis trimming allows partial words to be displayed,
+ /// while WordEllipsis removes whole words to fit.
+ /// Both guarantee to include an ellipsis ('...') at the end of the lines
+ /// where text has been trimmed as a result of line and column limits.
+ ///
+ public TextTrimming Trimming
+ {
+ set
+ {
+ if ((int)value < 0 || (int)value > (int)TextTrimming.WordEllipsis)
+ {
+ throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(TextTrimming));
+ }
+
+ _trimming = value;
+
+ _defaultParaProps.SetTextWrapping(_trimming == TextTrimming.None ?
+ TextWrapping.Wrap :
+ TextWrapping.WrapWithOverflow);
+
+ InvalidateMetrics();
+ }
+ get
+ {
+ return _trimming;
+ }
}
///
- /// Gets or sets the text wrapping.
+ /// Lazily initializes the cached metrics EXCEPT for black box metrics and
+ /// returns the CachedMetrics structure.
///
- public TextWrapping TextWrapping
+ private CachedMetrics Metrics
{
- get => _textWrapping;
- set => Set(ref _textWrapping, value);
+ get
+ {
+ return _metrics ??= DrawAndCalculateMetrics(
+ null, // drawing context
+ new Point(), // drawing offset
+ false);
+ }
}
///
- /// Gets platform-specific platform implementation.
+ /// Lazily initializes the cached metrics INCLUDING black box metrics and
+ /// returns the CachedMetrics structure.
///
- public IFormattedTextImpl PlatformImpl
+ private CachedMetrics BlackBoxMetrics
{
get
{
- if (_platformImpl == null)
+ if (_metrics == null || double.IsNaN(_metrics.Extent))
{
- _platformImpl = _platform.CreateFormattedText(
- _text ?? string.Empty,
- _typeface,
- _fontSize,
- _textAlignment,
- _textWrapping,
- _constraint,
- _spans);
+ // We need to obtain the metrics, including black box metrics.
+
+ _metrics = DrawAndCalculateMetrics(
+ null, // drawing context
+ new Point(), // drawing offset
+ true); // calculate black box metrics
}
+ return _metrics;
+ }
+ }
+
+ ///
+ /// The distance from the top of the first line to the bottom of the last line.
+ ///
+ public double Height
+ {
+ get
+ {
+ return Metrics.Height;
+ }
+ }
+
+ ///
+ /// The distance from the topmost black pixel of the first line
+ /// to the bottommost black pixel of the last line.
+ ///
+ public double Extent
+ {
+ get
+ {
+ return BlackBoxMetrics.Extent;
+ }
+ }
+
+ ///
+ /// The distance from the top of the first line to the baseline of the first line.
+ ///
+ public double Baseline
+ {
+ get
+ {
+ return Metrics.Baseline;
+ }
+ }
+
+ ///
+ /// The distance from the bottom of the last line to the extent bottom.
+ ///
+ public double OverhangAfter
+ {
+ get
+ {
+ return BlackBoxMetrics.OverhangAfter;
+ }
+ }
- return _platformImpl;
+ ///
+ /// The maximum distance from the leading black pixel to the leading alignment point of a line.
+ ///
+ public double OverhangLeading
+ {
+ get
+ {
+ return BlackBoxMetrics.OverhangLeading;
}
}
///
- /// Gets the lines in the text.
+ /// The maximum distance from the trailing black pixel to the trailing alignment point of a line.
///
- ///
- /// A collection of objects.
- ///
- public IEnumerable GetLines()
+ public double OverhangTrailing
+ {
+ get
{
- return PlatformImpl.GetLines();
+ return BlackBoxMetrics.OverhangTrailing;
+ }
}
///
- /// Hit tests a point in the text.
+ /// The maximum advance width between the leading and trailing alignment points of a line,
+ /// excluding the width of whitespace characters at the end of the line.
///
- /// The point.
- ///
- /// A describing the result of the hit test.
- ///
- public TextHitTestResult HitTestPoint(Point point)
+ public double Width
{
- return PlatformImpl.HitTestPoint(point);
+ get
+ {
+ return Metrics.Width;
+ }
}
///
- /// Gets the bounds rectangle that the specified character occupies.
+ /// The maximum advance width between the leading and trailing alignment points of a line,
+ /// including the width of whitespace characters at the end of the line.
///
- /// The index of the character.
- /// The character bounds.
- public Rect HitTestTextPosition(int index)
+ public double WidthIncludingTrailingWhitespace
{
- return PlatformImpl.HitTestTextPosition(index);
+ get
+ {
+ return Metrics.WidthIncludingTrailingWhitespace;
+ }
}
///
- /// Gets the bounds rectangles that the specified text range occupies.
+ /// Draws the text object
///
- /// The index of the first character.
- /// The number of characters in the text range.
- /// The character bounds.
- public IEnumerable HitTestTextRange(int index, int length)
+ internal void Draw(DrawingContext drawingContext, Point origin)
{
- return PlatformImpl.HitTestTextRange(index, length);
+ var lineOrigin = origin;
+
+ if (_metrics != null && !double.IsNaN(_metrics.Extent))
+ {
+ // we can't use foreach because it requires GetEnumerator and associated classes to be public
+ // foreach (TextLine currentLine in this)
+ using (var enumerator = GetEnumerator())
+ {
+ while (enumerator.MoveNext())
+ {
+ var currentLine = enumerator.Current!;
+
+ currentLine.Draw(drawingContext, lineOrigin);
+
+ AdvanceLineOrigin(ref lineOrigin, currentLine);
+ }
+ }
+ }
+ else
+ {
+ // Calculate metrics as we draw to avoid formatting again if we need metrics later; we compute
+ // black box metrics too because these are already known as a side-effect of drawing
+
+ _metrics = DrawAndCalculateMetrics(drawingContext, origin, true);
+ }
}
- private void Set(ref T field, T value)
+ private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics)
{
- if (EqualityComparer.Default.Equals(field, value))
+ var metrics = new CachedMetrics();
+
+ if (_text.Length == 0)
{
- return;
+ return metrics;
}
- field = value;
+ // we can't use foreach because it requires GetEnumerator and associated classes to be public
+ // foreach (TextLine currentLine in this)
+
+ using (var enumerator = GetEnumerator())
+ {
+ var first = true;
+
+ double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom;
+ accBlackBoxLeft = accBlackBoxTop = double.MaxValue;
+ accBlackBoxRight = accBlackBoxBottom = double.MinValue;
+
+ var origin = new Point(0, 0);
+
+ // Holds the TextLine.Start of the longest line. Thus it will hold the minimum value
+ // of TextLine.Start among all the lines that forms the text. The overhangs (leading and trailing)
+ // are calculated with an offset as a result of the same issue with TextLine.Start.
+ // So, we compute this offset and remove it later from the values of the overhangs.
+ var lineStartOfLongestLine = double.MaxValue;
- _platformImpl = null;
+ while (enumerator.MoveNext())
+ {
+ // enumerator will dispose the currentLine
+ var currentLine = enumerator.Current!;
+
+ // if we're drawing, do it first as this will compute black box metrics as a side-effect
+ if (drawingContext != null)
+ {
+ currentLine.Draw(drawingContext,
+ new Point(origin.X + drawingOffset.X, origin.Y + drawingOffset.Y));
+ }
+
+ if (getBlackBoxMetrics)
+ {
+ var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;
+ var blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing;
+ var blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter;
+ var blackBoxTop = blackBoxBottom - currentLine.Extent;
+
+ accBlackBoxLeft = Math.Min(accBlackBoxLeft, blackBoxLeft);
+ accBlackBoxRight = Math.Max(accBlackBoxRight, blackBoxRight);
+ accBlackBoxBottom = Math.Max(accBlackBoxBottom, blackBoxBottom);
+ accBlackBoxTop = Math.Min(accBlackBoxTop, blackBoxTop);
+
+ metrics.OverhangAfter = currentLine.OverhangAfter;
+ }
+
+ metrics.Height += currentLine.Height;
+ metrics.Width = Math.Max(metrics.Width, currentLine.Width);
+ metrics.WidthIncludingTrailingWhitespace = Math.Max(metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace);
+ lineStartOfLongestLine = Math.Min(lineStartOfLongestLine, currentLine.Start);
+
+ if (first)
+ {
+ metrics.Baseline = currentLine.Baseline;
+ first = false;
+ }
+
+ AdvanceLineOrigin(ref origin, currentLine);
+ }
+
+ if (getBlackBoxMetrics)
+ {
+ metrics.Extent = accBlackBoxBottom - accBlackBoxTop;
+ metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine;
+ metrics.OverhangTrailing = metrics.Width - (accBlackBoxRight - lineStartOfLongestLine);
+ }
+ else
+ {
+ // indicate that black box metrics are not known
+ metrics.Extent = double.NaN;
+ }
+ }
+
+ return metrics;
+ }
+
+ private class TextSourceImplementation : ITextSource
+ {
+ private readonly FormattedText _that;
+
+ public TextSourceImplementation(FormattedText text)
+ {
+ _that = text;
+ }
+
+ ///
+ public TextRun? GetTextRun(int textSourceCharacterIndex)
+ {
+ if (textSourceCharacterIndex >= _that._text.Length)
+ {
+ return null;
+ }
+
+ var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);
+
+ TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
+
+ var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length,
+ properties);
+
+ return textCharacters;
+ }
}
}
}
diff --git a/src/Avalonia.Visuals/Media/FormattedTextLine.cs b/src/Avalonia.Visuals/Media/FormattedTextLine.cs
deleted file mode 100644
index 42859f698ab..00000000000
--- a/src/Avalonia.Visuals/Media/FormattedTextLine.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-namespace Avalonia.Media
-{
- ///
- /// Stores information about a line of .
- ///
- public class FormattedTextLine
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The length of the line, in characters.
- /// The height of the line, in pixels.
- public FormattedTextLine(int length, double height)
- {
- Length = length;
- Height = height;
- }
-
- ///
- /// Gets the length of the line, in characters.
- ///
- public int Length { get; }
-
- ///
- /// Gets the height of the line, in pixels.
- ///
- public double Height { get; }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs b/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs
deleted file mode 100644
index fcb631d1eb4..00000000000
--- a/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-namespace Avalonia.Media
-{
- ///
- /// Describes the formatting for a span of text in a object.
- ///
- public class FormattedTextStyleSpan
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The index of the first character in the span.
- /// The length of the span.
- /// The span's foreground brush.
- public FormattedTextStyleSpan(
- int startIndex,
- int length,
- IBrush? foregroundBrush = null)
- {
- StartIndex = startIndex;
- Length = length;
- ForegroundBrush = foregroundBrush;
- }
-
- ///
- /// Gets the index of the first character in the span.
- ///
- public int StartIndex { get; }
-
- ///
- /// Gets the length of the span.
- ///
- public int Length { get; }
-
- ///
- /// Gets the span's foreground brush.
- ///
- public IBrush? ForegroundBrush { get; }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs
index 53b35fb31b0..ef5ffb8d781 100644
--- a/src/Avalonia.Visuals/Media/GlyphRun.cs
+++ b/src/Avalonia.Visuals/Media/GlyphRun.cs
@@ -11,8 +11,8 @@ 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 static readonly IComparer s_ascendingComparer = Comparer.Default;
+ private static readonly IComparer s_descendingComparer = new ReverseComparer();
private IGlyphRunImpl? _glyphRunImpl;
private GlyphTypeface _glyphTypeface;
@@ -21,12 +21,13 @@ public sealed class GlyphRun : IDisposable
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
- private ReadOnlySlice _glyphIndices;
- private ReadOnlySlice _glyphAdvances;
- private ReadOnlySlice _glyphOffsets;
- private ReadOnlySlice _glyphClusters;
private ReadOnlySlice _characters;
+ private IReadOnlyList _glyphIndices;
+ private IReadOnlyList? _glyphAdvances;
+ private IReadOnlyList? _glyphOffsets;
+ private IReadOnlyList? _glyphClusters;
+
///
/// Initializes a new instance of the class by specifying properties of the class.
///
@@ -41,25 +42,25 @@ public sealed class GlyphRun : IDisposable
public GlyphRun(
GlyphTypeface glyphTypeface,
double fontRenderingEmSize,
- ReadOnlySlice glyphIndices,
- ReadOnlySlice glyphAdvances = default,
- ReadOnlySlice glyphOffsets = default,
- ReadOnlySlice characters = default,
- ReadOnlySlice glyphClusters = default,
+ ReadOnlySlice characters,
+ IReadOnlyList glyphIndices,
+ IReadOnlyList? glyphAdvances = null,
+ IReadOnlyList? glyphOffsets = null,
+ IReadOnlyList? glyphClusters = null,
int biDiLevel = 0)
{
- _glyphTypeface = glyphTypeface;
+ _glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
- GlyphIndices = glyphIndices;
+ Characters = characters;
+
+ _glyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets;
- Characters = characters;
-
GlyphClusters = glyphClusters;
BiDiLevel = biDiLevel;
@@ -114,7 +115,7 @@ public Point BaselineOrigin
///
/// Gets or sets an array of values that represent the glyph indices in the rendering physical font.
///
- public ReadOnlySlice GlyphIndices
+ public IReadOnlyList GlyphIndices
{
get => _glyphIndices;
set => Set(ref _glyphIndices, value);
@@ -123,7 +124,7 @@ public ReadOnlySlice GlyphIndices
///
/// Gets or sets an array of values that represent the advances corresponding to the glyph indices.
///
- public ReadOnlySlice GlyphAdvances
+ public IReadOnlyList? GlyphAdvances
{
get => _glyphAdvances;
set => Set(ref _glyphAdvances, value);
@@ -132,7 +133,7 @@ public ReadOnlySlice GlyphAdvances
///
/// Gets or sets an array of values representing the offsets of the glyphs in the .
///
- public ReadOnlySlice GlyphOffsets
+ public IReadOnlyList? GlyphOffsets
{
get => _glyphOffsets;
set => Set(ref _glyphOffsets, value);
@@ -150,7 +151,7 @@ public ReadOnlySlice Characters
///
/// Gets or sets a list of values representing a mapping from character index to glyph index.
///
- public ReadOnlySlice GlyphClusters
+ public IReadOnlyList? GlyphClusters
{
get => _glyphClusters;
set => Set(ref _glyphClusters, value);
@@ -202,34 +203,73 @@ public IGlyphRunImpl GlyphRunImpl
///
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
+ var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
var distance = 0.0;
- if (characterHit.FirstCharacterIndex + characterHit.TrailingLength > Characters.End)
+ if (IsLeftToRight)
{
- return Size.Width;
- }
-
- var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex);
+ if (GlyphClusters != null)
+ {
+ if (characterIndex < GlyphClusters[0])
+ {
+ return 0;
+ }
- if (!GlyphClusters.IsEmpty)
- {
- var currentCluster = GlyphClusters[glyphIndex];
+ if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
+ {
+ return Metrics.WidthIncludingTrailingWhitespace;
+ }
+ }
- if (characterHit.TrailingLength > 0)
+ var glyphIndex = FindGlyphIndex(characterIndex);
+
+ if (GlyphClusters != null)
{
- while (glyphIndex < GlyphClusters.Length && GlyphClusters[glyphIndex] == currentCluster)
+ var currentCluster = GlyphClusters[glyphIndex];
+
+ //Move to the end of the glyph cluster
+ if (characterHit.TrailingLength > 0)
{
- glyphIndex++;
+ while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster)
+ {
+ glyphIndex++;
+ }
}
}
- }
- for (var i = 0; i < glyphIndex; i++)
- {
- distance += GetGlyphAdvance(i);
+ for (var i = 0; i < glyphIndex; i++)
+ {
+ distance += GetGlyphAdvance(i, out _);
+ }
+
+ return distance;
}
+ else
+ {
+ //RightToLeft
+ var glyphIndex = FindGlyphIndex(characterIndex);
+
+ if (GlyphClusters != null)
+ {
+ if (characterIndex > GlyphClusters[0])
+ {
+ return 0;
+ }
+
+ if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
+ {
+ return Size.Width;
+ }
+ }
- return distance;
+ for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++)
+ {
+ distance += GetGlyphAdvance(i, out _);
+ }
+
+ return Size.Width - distance;
+ }
}
///
@@ -243,50 +283,86 @@ public double GetDistanceFromCharacterHit(CharacterHit characterHit)
///
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
+ var characterIndex = 0;
+
// Before
- if (distance < 0)
+ if (distance <= 0)
{
isInside = false;
- var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _);
+ if(GlyphClusters != null)
+ {
+ characterIndex = GlyphClusters[characterIndex];
+ }
+
+ var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
}
//After
- if (distance > Size.Width)
+ if (distance >= Size.Width)
{
isInside = false;
- var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _);
+ characterIndex = GlyphIndices.Count - 1;
+
+ if(GlyphClusters != null)
+ {
+ characterIndex = GlyphClusters[characterIndex];
+ }
+
+ var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
}
//Within
- var currentX = 0.0;
- var index = 0;
+ var currentX = 0d;
- for (; index < GlyphIndices.Length - Metrics.NewlineLength; index++)
+ if (IsLeftToRight)
{
- var advance = GetGlyphAdvance(index);
-
- if (currentX + advance >= distance)
+ for (var index = 0; index < GlyphIndices.Count; index++)
{
- break;
- }
+ var advance = GetGlyphAdvance(index, out var cluster);
+
+ characterIndex = cluster;
+
+ if (distance > currentX && distance <= currentX + advance)
+ {
+ break;
+ }
- currentX += advance;
+ currentX += advance;
+ }
}
+ else
+ {
+ currentX = Size.Width;
- var characterHit =
- FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
+ for (var index = GlyphIndices.Count - 1; index >= 0; index--)
+ {
+ var advance = GetGlyphAdvance(index, out var cluster);
- var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
+ characterIndex = cluster;
+
+ if (currentX - advance < distance)
+ {
+ break;
+ }
+
+ currentX -= advance;
+ }
+ }
isInside = true;
- var isTrailing = distance > offset + width / 2;
+ var characterHit = FindNearestCharacterHit(characterIndex, out var width);
+
+ var delta = width / 2;
+ var offset = IsLeftToRight ? distance - currentX : currentX - distance;
+
+ var isTrailing = offset > delta;
return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
}
@@ -303,13 +379,21 @@ public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
if (characterHit.TrailingLength == 0)
{
- return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
+ characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
+
+ var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ return textPosition > _characters.End ?
+ characterHit :
+ new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
- return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
+ return nextCharacterHit == characterHit ?
+ characterHit :
+ new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}
///
@@ -327,9 +411,9 @@ public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
return new CharacterHit(characterHit.FirstCharacterIndex);
}
- return characterHit.FirstCharacterIndex == Characters.Start ?
- new CharacterHit(Characters.Start) :
- FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
+ var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
+
+ return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
}
///
@@ -341,7 +425,7 @@ public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
///
public int FindGlyphIndex(int characterIndex)
{
- if (GlyphClusters.IsEmpty)
+ if (GlyphClusters == null)
{
return characterIndex;
}
@@ -353,16 +437,16 @@ public int FindGlyphIndex(int characterIndex)
return 0;
}
- if (characterIndex > GlyphClusters[GlyphClusters.Length - 1])
+ if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{
- return _glyphClusters.Length - 1;
+ return GlyphClusters.Count - 1;
}
}
else
{
- if (characterIndex < GlyphClusters[GlyphClusters.Length - 1])
+ if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
{
- return _glyphClusters.Length - 1;
+ return GlyphClusters.Count - 1;
}
if (characterIndex > GlyphClusters[0])
@@ -373,10 +457,10 @@ public int FindGlyphIndex(int characterIndex)
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
- var clusters = GlyphClusters.Buffer.Span;
+ var clusters = GlyphClusters;
// Find the start of the cluster at the character index.
- var start = clusters.BinarySearch((ushort)characterIndex, comparer);
+ var start = clusters.BinarySearch(characterIndex, comparer);
// No cluster found.
if (start < 0)
@@ -385,7 +469,7 @@ public int FindGlyphIndex(int characterIndex)
{
characterIndex--;
- start = clusters.BinarySearch((ushort)characterIndex, comparer);
+ start = clusters.BinarySearch(characterIndex, comparer);
}
if (start < 0)
@@ -403,7 +487,7 @@ public int FindGlyphIndex(int characterIndex)
}
else
{
- while (start + 1 < clusters.Length && clusters[start + 1] == clusters[start])
+ while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start])
{
start++;
}
@@ -426,9 +510,9 @@ public CharacterHit FindNearestCharacterHit(int index, out double width)
var start = FindGlyphIndex(index);
- if (GlyphClusters.IsEmpty)
+ if (GlyphClusters == null)
{
- width = GetGlyphAdvance(index);
+ width = GetGlyphAdvance(index, out _);
return new CharacterHit(start, 1);
}
@@ -441,13 +525,13 @@ public CharacterHit FindNearestCharacterHit(int index, out double width)
while (nextCluster == cluster)
{
- width += GetGlyphAdvance(currentIndex);
+ width += GetGlyphAdvance(currentIndex, out _);
if (IsLeftToRight)
{
currentIndex++;
- if (currentIndex == GlyphClusters.Length)
+ if (currentIndex == GlyphClusters.Count)
{
break;
}
@@ -483,10 +567,13 @@ public CharacterHit FindNearestCharacterHit(int index, out double width)
/// Gets a glyph's width.
///
/// The glyph index.
+ /// The current cluster.
/// The glyph's width.
- private double GetGlyphAdvance(int index)
+ private double GetGlyphAdvance(int index, out int cluster)
{
- if (!GlyphAdvances.IsEmpty)
+ cluster = GlyphClusters != null ? GlyphClusters[index] : index;
+
+ if (GlyphAdvances != null)
{
return GlyphAdvances[index];
}
@@ -508,42 +595,51 @@ private Point CalculateBaselineOrigin()
private GlyphRunMetrics CreateGlyphRunMetrics()
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
-
var widthIncludingTrailingWhitespace = 0d;
- var width = 0d;
- var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength);
-
- for (var index = 0; index < _glyphIndices.Length; index++)
+ var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
+
+ for (var index = 0; index < GlyphIndices.Count; index++)
{
- var advance = GetGlyphAdvance(index);
+ var advance = GetGlyphAdvance(index, out _);
widthIncludingTrailingWhitespace += advance;
+ }
+
+ var width = widthIncludingTrailingWhitespace;
- if (index > _glyphIndices.Length - 1 - trailingWhitespaceLength)
+ if (IsLeftToRight)
+ {
+ for (var index = GlyphIndices.Count - glyphCount; index = 0;)
{
@@ -562,16 +658,26 @@ private int GetTrailingWhitespaceLength(out int newLineLength)
trailingWhitespaceLength++;
i -= count;
+ glyphCount++;
}
}
else
{
- for (var i = _glyphClusters.Length - 1; i >= 0; i--)
+ for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
- var cluster = _glyphClusters[i];
+ var cluster = GlyphClusters[i];
var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
+ if (codepointIndex < 0)
+ {
+ trailingWhitespaceLength = _characters.Length;
+
+ glyphCount = GlyphClusters.Count;
+
+ break;
+ }
+
var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
if (!codepoint.IsWhiteSpace)
@@ -585,6 +691,8 @@ private int GetTrailingWhitespaceLength(out int newLineLength)
}
trailingWhitespaceLength++;
+
+ glyphCount++;
}
}
@@ -610,19 +718,19 @@ private void Set(ref T field, T value)
///
private void Initialize()
{
- if (GlyphIndices.Length == 0)
+ if (GlyphIndices == null)
{
throw new InvalidOperationException();
}
- var glyphCount = GlyphIndices.Length;
+ var glyphCount = GlyphIndices.Count;
- if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount)
+ if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount)
{
throw new InvalidOperationException();
}
- if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount)
+ if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount)
{
throw new InvalidOperationException();
}
diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
index 67dfbb84b6f..45ef04e77f7 100644
--- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs
+++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
@@ -5,8 +5,6 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
- public const int InvisibleGlyph = 3;
-
public GlyphTypeface(Typeface typeface)
: this(FontManager.Current.PlatformImpl.CreateGlyphTypeface(typeface))
{
diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs
index 57936426f36..8eeb86c555a 100644
--- a/src/Avalonia.Visuals/Media/TextDecoration.cs
+++ b/src/Avalonia.Visuals/Media/TextDecoration.cs
@@ -154,11 +154,12 @@ public TextDecorationUnit StrokeOffsetUnit
/// Draws the at given origin.
///
/// The drawing context.
- /// The shaped characters that are decorated.
- internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters)
+ /// The decorated run.
+ /// The font metrics of the decorated run.
+ /// The default brush that is used to draw the decoration.
+ internal void Draw(DrawingContext drawingContext, GlyphRun glyphRun, FontMetrics fontMetrics, IBrush defaultBrush)
{
- var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize;
- var fontMetrics = shapedTextCharacters.FontMetrics;
+ var baselineOrigin = glyphRun.BaselineOrigin;
var thickness = StrokeThickness;
switch (StrokeThicknessUnit)
@@ -176,7 +177,7 @@ internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTex
break;
case TextDecorationUnit.FontRenderingEmSize:
- thickness = fontRenderingEmSize * thickness;
+ thickness = fontMetrics.FontRenderingEmSize * thickness;
break;
}
@@ -185,32 +186,30 @@ internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTex
switch (Location)
{
case TextDecorationLocation.Baseline:
- origin += shapedTextCharacters.GlyphRun.BaselineOrigin;
+ origin += glyphRun.BaselineOrigin;
break;
case TextDecorationLocation.Strikethrough:
- origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X,
- shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.StrikethroughPosition);
+ origin += new Point(baselineOrigin.X, baselineOrigin.Y + fontMetrics.StrikethroughPosition);
break;
case TextDecorationLocation.Underline:
- origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X,
- shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.UnderlinePosition);
+ origin += new Point(baselineOrigin.X, baselineOrigin.Y + fontMetrics.UnderlinePosition);
break;
}
switch (StrokeOffsetUnit)
{
case TextDecorationUnit.FontRenderingEmSize:
- origin += new Point(0, StrokeOffset * fontRenderingEmSize);
+ origin += new Point(0, StrokeOffset * fontMetrics.FontRenderingEmSize);
break;
case TextDecorationUnit.Pixel:
origin += new Point(0, StrokeOffset);
break;
}
- var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness,
+ var pen = new Pen(Stroke ?? defaultBrush, thickness,
new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap);
- drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Size.Width, 0));
+ drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Size.Width, 0));
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs
index dd91dc04bd9..e01bba00a4f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs
@@ -5,11 +5,13 @@
///
public readonly struct FontMetrics
{
- public FontMetrics(Typeface typeface, double fontSize)
+ public FontMetrics(Typeface typeface, double fontRenderingEmSize)
{
var glyphTypeface = typeface.GlyphTypeface;
- var scale = fontSize / glyphTypeface.DesignEmHeight;
+ var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
+
+ FontRenderingEmSize = fontRenderingEmSize;
Ascent = glyphTypeface.Ascent * scale;
@@ -28,6 +30,11 @@ public FontMetrics(Typeface typeface, double fontSize)
StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale;
}
+ ///
+ /// Em size of font used to format and display text
+ ///
+ public double FontRenderingEmSize { get; }
+
///
/// Gets the recommended distance above the baseline.
///
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
similarity index 83%
rename from tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
rename to src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
index e3a2f6e7668..98344141f16 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
@@ -1,27 +1,27 @@
using System;
using System.Collections.Generic;
-using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
-namespace Avalonia.Skia.UnitTests.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
{
internal readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
private readonly TextRunProperties _defaultProperties;
- private readonly IReadOnlyList> _textModifier;
+ private readonly IReadOnlyList>? _textModifier;
public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties,
- IReadOnlyList> textModifier)
+ IReadOnlyList>? textModifier)
{
_text = text;
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
- public TextRun GetTextRun(int textSourceIndex)
+ public TextRun? GetTextRun(int textSourceIndex)
{
- if (textSourceIndex > _text.End)
+ if (textSourceIndex > _text.Length)
{
return null;
}
@@ -48,7 +48,7 @@ public TextRun GetTextRun(int textSourceIndex)
/// The created text style run.
///
private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
- TextRunProperties defaultProperties, IReadOnlyList> textModifier)
+ TextRunProperties defaultProperties, IReadOnlyList>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
{
@@ -69,7 +69,7 @@ private static ValueSpan CreateTextStyleRun(ReadOnlySlice CreateTextStyleRun(ReadOnlySlice CreateTextStyleRun(ReadOnlySlice
public sealed class ShapeableTextCharacters : TextRun
{
- public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties properties)
+ public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties properties, sbyte biDiLevel)
{
TextSourceLength = text.Length;
Text = text;
Properties = properties;
+ BidiLevel = biDiLevel;
}
public override int TextSourceLength { get; }
@@ -19,5 +20,7 @@ public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties prope
public override ReadOnlySlice Text { get; }
public override TextRunProperties Properties { get; }
+
+ public sbyte BidiLevel { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs
new file mode 100644
index 00000000000..ee38cf39e06
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+ public sealed class ShapedBuffer : IList
+ {
+ private static readonly IComparer s_clusterComparer = new CompareClusters();
+
+ public ShapedBuffer(ReadOnlySlice text, int length, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
+ : this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel)
+ {
+
+ }
+
+ internal ShapedBuffer(ReadOnlySlice text, ArraySlice glyphInfos, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
+ {
+ Text = text;
+ GlyphInfos = glyphInfos;
+ GlyphTypeface = glyphTypeface;
+ FontRenderingEmSize = fontRenderingEmSize;
+ BidiLevel = bidiLevel;
+ }
+
+ internal ArraySlice GlyphInfos { get; }
+
+ public ReadOnlySlice Text { get; }
+
+ public int Length => GlyphInfos.Length;
+
+ public GlyphTypeface GlyphTypeface { get; }
+
+ public double FontRenderingEmSize { get; }
+
+ 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);
+
+ ///
+ /// Finds a glyph index for given character index.
+ ///
+ /// The character index.
+ ///
+ /// The glyph index.
+ ///
+ private int FindGlyphIndex(int characterIndex)
+ {
+ if (characterIndex < GlyphInfos[0].GlyphCluster)
+ {
+ return 0;
+ }
+
+ if (characterIndex > GlyphInfos[GlyphInfos.Length - 1].GlyphCluster)
+ {
+ return GlyphInfos.Length - 1;
+ }
+
+
+ var comparer = s_clusterComparer;
+
+ var clusters = GlyphInfos.Span;
+
+ var searchValue = new GlyphInfo(0, characterIndex);
+
+ var start = clusters.BinarySearch(searchValue, comparer);
+
+ if (start < 0)
+ {
+ while (characterIndex > 0 && start < 0)
+ {
+ characterIndex--;
+
+ searchValue = new GlyphInfo(0, characterIndex);
+
+ start = clusters.BinarySearch(searchValue, comparer);
+ }
+
+ if (start < 0)
+ {
+ return -1;
+ }
+ }
+
+ while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster)
+ {
+ start--;
+ }
+
+ return start;
+ }
+
+ ///
+ /// Splits the at specified length.
+ ///
+ /// The length.
+ /// The split result.
+ internal SplitResult Split(int length)
+ {
+ var glyphCount = FindGlyphIndex(Text.Start + length);
+
+ if (Text.Length == length)
+ {
+ return new SplitResult(this, null);
+ }
+
+ if (Text.Length == glyphCount)
+ {
+ return new SplitResult(this, null);
+ }
+
+ var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
+
+ var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
+
+ return new SplitResult(first, second);
+ }
+
+ int ICollection.Count => throw new NotImplementedException();
+
+ bool ICollection.IsReadOnly => true;
+
+ public GlyphInfo this[int index]
+ {
+ get => GlyphInfos[index];
+ 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();
+ }
+ }
+
+ public readonly 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.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index 72bb7431d7c..96b3857098e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -1,4 +1,6 @@
-using Avalonia.Utilities;
+using System;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@@ -7,15 +9,23 @@ namespace Avalonia.Media.TextFormatting
///
public sealed class ShapedTextCharacters : DrawableTextRun
{
- public ShapedTextCharacters(GlyphRun glyphRun, TextRunProperties properties)
+ private GlyphRun? _glyphRun;
+
+ public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
- Text = glyphRun.Characters;
+ ShapedBuffer = shapedBuffer;
+ Text = shapedBuffer.Text;
Properties = properties;
TextSourceLength = Text.Length;
- FontMetrics = new FontMetrics(Properties.Typeface, Properties.FontRenderingEmSize);
- GlyphRun = glyphRun;
+ FontMetrics = new FontMetrics(properties.Typeface, properties.FontRenderingEmSize);
}
+ public bool IsReversed { get; private set; }
+
+ public sbyte BidiLevel => ShapedBuffer.BidiLevel;
+
+ public ShapedBuffer ShapedBuffer { get; }
+
///
public override ReadOnlySlice Text { get; }
@@ -25,31 +35,29 @@ public ShapedTextCharacters(GlyphRun glyphRun, TextRunProperties properties)
///
public override int TextSourceLength { get; }
- ///
+ public FontMetrics FontMetrics { get; }
+
public override Size Size => GlyphRun.Size;
- ///
- /// Gets the font metrics.
- ///
- ///
- /// The font metrics.
- ///
- public FontMetrics FontMetrics { get; }
+ public GlyphRun GlyphRun
+ {
+ get
+ {
+ if(_glyphRun is null)
+ {
+ _glyphRun = CreateGlyphRun();
+ }
- ///
- /// Gets the glyph run.
- ///
- ///
- /// The glyphs.
- ///
- public GlyphRun GlyphRun { get; }
+ return _glyphRun;
+ }
+ }
///
public override void Draw(DrawingContext drawingContext, Point origin)
{
using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
{
- if (GlyphRun.GlyphIndices.Length == 0)
+ if (GlyphRun.GlyphIndices.Count == 0)
{
return;
}
@@ -78,116 +86,97 @@ public override void Draw(DrawingContext drawingContext, Point origin)
foreach (var textDecoration in Properties.TextDecorations)
{
- textDecoration.Draw(drawingContext, this);
+ textDecoration.Draw(drawingContext, GlyphRun, FontMetrics, Properties.ForegroundBrush);
}
}
}
+ internal void Reverse()
+ {
+ _glyphRun = null;
+
+ ShapedBuffer.GlyphInfos.Span.Reverse();
+
+ IsReversed = !IsReversed;
+ }
+
///
- /// Splits the at specified length.
+ /// Measures the number of characters that fit into available width.
///
- /// The length.
- /// The split result.
- public SplitTextCharactersResult Split(int length)
+ /// The available width.
+ /// The count of fitting characters.
+ ///
+ /// true if characters fit into the available width; otherwise, false.
+ ///
+ internal bool TryMeasureCharacters(double availableWidth, out int length)
{
- var glyphCount = GlyphRun.IsLeftToRight ?
- GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length) :
- GlyphRun.FindGlyphIndex(GlyphRun.Characters.End - length);
+ length = 0;
+ var currentWidth = 0.0;
- if (GlyphRun.Characters.Length == length)
+ for (var i = 0; i < ShapedBuffer.Length; i++)
{
- return new SplitTextCharactersResult(this, null);
+ var advance = ShapedBuffer.GlyphAdvances[i];
+
+ if (currentWidth + advance > availableWidth)
+ {
+ break;
+ }
+
+ Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
+
+ length += count;
+ currentWidth += advance;
}
- if (GlyphRun.GlyphIndices.Length == glyphCount)
+ return length > 0;
+ }
+
+ internal SplitResult Split(int length)
+ {
+ if (IsReversed)
{
- return new SplitTextCharactersResult(this, null);
+ Reverse();
}
- if (GlyphRun.IsLeftToRight)
+ if(length == 0)
{
- var firstGlyphRun = new GlyphRun(
- Properties.Typeface.GlyphTypeface,
- Properties.FontRenderingEmSize,
- GlyphRun.GlyphIndices.Take(glyphCount),
- GlyphRun.GlyphAdvances.Take(glyphCount),
- GlyphRun.GlyphOffsets.Take(glyphCount),
- GlyphRun.Characters.Take(length),
- GlyphRun.GlyphClusters.Take(glyphCount),
- GlyphRun.BiDiLevel);
-
- var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
-
- var secondGlyphRun = new GlyphRun(
- Properties.Typeface.GlyphTypeface,
- Properties.FontRenderingEmSize,
- GlyphRun.GlyphIndices.Skip(glyphCount),
- GlyphRun.GlyphAdvances.Skip(glyphCount),
- GlyphRun.GlyphOffsets.Skip(glyphCount),
- GlyphRun.Characters.Skip(length),
- GlyphRun.GlyphClusters.Skip(glyphCount),
- GlyphRun.BiDiLevel);
-
- var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
-
- return new SplitTextCharactersResult(firstTextRun, secondTextRun);
+ throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero.");
}
- else
+
+ if(length == ShapedBuffer.Length)
{
- var take = GlyphRun.GlyphIndices.Length - glyphCount;
-
- var firstGlyphRun = new GlyphRun(
- Properties.Typeface.GlyphTypeface,
- Properties.FontRenderingEmSize,
- GlyphRun.GlyphIndices.Take(take),
- GlyphRun.GlyphAdvances.Take(take),
- GlyphRun.GlyphOffsets.Take(take),
- GlyphRun.Characters.Skip(length),
- GlyphRun.GlyphClusters.Take(take),
- GlyphRun.BiDiLevel);
-
- var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
-
- var secondGlyphRun = new GlyphRun(
- Properties.Typeface.GlyphTypeface,
- Properties.FontRenderingEmSize,
- GlyphRun.GlyphIndices.Skip(take),
- GlyphRun.GlyphAdvances.Skip(take),
- GlyphRun.GlyphOffsets.Skip(take),
- GlyphRun.Characters.Take(length),
- GlyphRun.GlyphClusters.Skip(take),
- GlyphRun.BiDiLevel);
-
- var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
-
- return new SplitTextCharactersResult(secondTextRun,firstTextRun);
+ return new SplitResult(this, null);
}
- }
- public readonly struct SplitTextCharactersResult
- {
- public SplitTextCharactersResult(ShapedTextCharacters first, ShapedTextCharacters? second)
- {
- First = first;
+ var splitBuffer = ShapedBuffer.Split(length);
+
+ var first = new ShapedTextCharacters(splitBuffer.First, Properties);
+
+ #if DEBUG
- Second = second;
+ if (first.Text.Length != length)
+ {
+ throw new InvalidOperationException("Split length mismatch.");
}
+
+ #endif
+
+ var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
- ///
- /// Gets the first text run.
- ///
- ///
- /// The first text run.
- ///
- public ShapedTextCharacters First { get; }
-
- ///
- /// Gets the second text run.
- ///
- ///
- /// The second text run.
- ///
- public ShapedTextCharacters? Second { get; }
+ return new SplitResult(first, second);
+ }
+
+ internal GlyphRun CreateGlyphRun()
+ {
+ return new GlyphRun(
+ ShapedBuffer.GlyphTypeface,
+ ShapedBuffer.FontRenderingEmSize,
+ Text,
+ ShapedBuffer.GlyphIndices,
+ ShapedBuffer.GlyphAdvances,
+ ShapedBuffer.GlyphOffsets,
+ ShapedBuffer.GlyphClusters,
+ BidiLevel);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Visuals/Media/TextFormatting/SplitResult.cs
new file mode 100644
index 00000000000..02c71744996
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/SplitResult.cs
@@ -0,0 +1,28 @@
+namespace Avalonia.Media.TextFormatting
+{
+ internal readonly struct SplitResult
+ {
+ public SplitResult(T first, T? second)
+ {
+ First = first;
+
+ Second = second;
+ }
+
+ ///
+ /// Gets the first part.
+ ///
+ ///
+ /// The first part.
+ ///
+ public T First { get; }
+
+ ///
+ /// Gets the second part.
+ ///
+ ///
+ /// The second part.
+ ///
+ public T? Second { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index cfca8f9ab2a..faa73719c81 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@@ -37,19 +38,20 @@ public TextCharacters(ReadOnlySlice text, int offsetToFirstCharacter, int
/// Gets a list of .
///
/// The shapeable text characters.
- internal IList GetShapeableCharacters()
+ internal IList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel,
+ ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List(2);
- var runText = Text;
-
while (!runText.IsEmpty)
{
- var shapeableRun = CreateShapeableRun(runText, Properties);
+ var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
runText = runText.Skip(shapeableRun.Text.Length);
+
+ previousProperties = shapeableRun.Properties;
}
return shapeableCharacters;
@@ -60,34 +62,70 @@ internal IList GetShapeableCharacters()
///
/// The text to create text runs from.
/// The default text run properties.
+ /// The bidi level of the run.
+ ///
/// A list of shapeable text runs.
- private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties)
+ private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text,
+ TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
-
var currentTypeface = defaultTypeface;
+ var previousTypeface = previousProperties?.Typeface;
- if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count))
+ if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
{
+ if (script == Script.Common && previousTypeface is not null)
+ {
+ if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
+ {
+ return new ShapeableTextCharacters(text.Take(fallbackCount),
+ new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ }
+ }
+
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush));
-
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ }
+
+ if (previousTypeface is not null)
+ {
+ if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
+ {
+ return new ShapeableTextCharacters(text.Take(count),
+ new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+ }
}
- var codepoint = Codepoint.ReadAt(text, count, out _);
+ var codepoint = Codepoint.ReplacementCodepoint;
+
+ var codepointEnumerator = new CodepointEnumerator(text.Skip(count));
+ while (codepointEnumerator.MoveNext())
+ {
+ if (codepointEnumerator.Current.IsWhiteSpace)
+ {
+ continue;
+ }
+
+ codepoint = codepointEnumerator.Current;
+
+ break;
+ }
+
//ToDo: Fix FontFamily fallback
var matchFound =
FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface);
- if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
+ if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush));
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
}
// no fallback found
@@ -111,35 +149,30 @@ private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, Tex
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
- defaultProperties.TextDecorations, defaultProperties.ForegroundBrush));
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
}
///
- /// Tries to get run properties.
+ /// Tries to get a shapeable length that is supported by the specified typeface.
///
- ///
- ///
+ /// The text.
/// The typeface that is used to find matching characters.
- ///
+ /// The shapeable length.
+ ///
///
- protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface,
- out int count)
+ protected static bool TryGetShapeableLength(ReadOnlySlice text, Typeface typeface, out int length,
+ out Script script)
{
+ length = 0;
+ script = Script.Unknown;
+
if (text.Length == 0)
{
- count = 0;
return false;
}
- var isFallback = typeface != defaultTypeface;
-
- count = 0;
- var script = Script.Unknown;
- var direction = BiDiClass.LeftToRight;
-
var font = typeface.GlyphTypeface;
- var defaultFont = defaultTypeface.GlyphTypeface;
-
+
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
@@ -148,20 +181,10 @@ protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface,
var currentScript = currentGrapheme.FirstCodepoint.Script;
- var currentDirection = currentGrapheme.FirstCodepoint.BiDiClass;
-
- //// ToDo: Implement BiDi algorithm
- //if (currentScript.HorizontalDirection != direction)
- //{
- // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
- // {
- // break;
- // }
- //}
-
if (currentScript != script)
{
- if (script is Script.Unknown)
+ if (script is Script.Unknown || currentScript != Script.Common &&
+ script is Script.Common or Script.Inherited)
{
script = currentScript;
}
@@ -174,37 +197,16 @@ protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface,
}
}
- //Only handle non whitespace here
- if (!currentGrapheme.FirstCodepoint.IsWhiteSpace)
- {
- //Stop at the first glyph that is present in the default typeface.
- if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
- {
- break;
- }
-
- //Stop at the first missing glyph
- if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
- {
- break;
- }
- }
-
- if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
- {
- break;
- }
-
- if (direction == BiDiClass.RightToLeft && currentDirection == BiDiClass.CommonSeparator)
+ //Stop at the first missing glyph
+ if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
- count += currentGrapheme.Text.Length;
- direction = currentDirection;
+ length += currentGrapheme.Text.Length;
}
- return count > 0;
+ return length > 0;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs
index 682fd930f67..4d342d4d58f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs
@@ -5,5 +5,14 @@
///
public class TextEndOfParagraph : TextEndOfLine
{
+ public TextEndOfParagraph()
+ {
+
+ }
+
+ public TextEndOfParagraph(int textSourceLength) : base(textSourceLength)
+ {
+
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index c97e36d5ff6..101f2737983 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@@ -11,172 +13,56 @@ public override TextLine FormatLine(ITextSource textSource, int firstTextSourceI
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{
var textWrapping = paragraphProperties.TextWrapping;
+ FlowDirection flowDirection;
+ TextLineBreak? nextLineBreak = null;
+ List shapedRuns;
- var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak,
- out var nextLineBreak);
+ var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
+ out var textEndOfLine, out var textRange);
- var textRange = GetTextRange(textRuns);
-
- TextLine textLine;
-
- switch (textWrapping)
- {
- case TextWrapping.NoWrap:
- {
- textLine = new TextLineImpl(textRuns, textRange, paragraphWidth, paragraphProperties,
- nextLineBreak);
- break;
- }
- case TextWrapping.WrapWithOverflow:
- case TextWrapping.Wrap:
- {
- textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
- nextLineBreak);
- break;
- }
- default:
- throw new ArgumentOutOfRangeException();
- }
-
- return textLine;
- }
-
- ///
- /// Measures the number of characters that fit into available width.
- ///
- /// The text run.
- /// The available width.
- /// The count of fitting characters.
- ///
- /// true if characters fit into the available width; otherwise, false.
- ///
- internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth,
- out int count)
- {
- var glyphRun = textCharacters.GlyphRun;
-
- if (glyphRun.Size.Width < availableWidth)
+ if (previousLineBreak?.RemainingCharacters != null)
{
- count = glyphRun.Characters.Length;
-
- return true;
+ flowDirection = previousLineBreak.FlowDirection;
+ shapedRuns = previousLineBreak.RemainingCharacters.ToList();
+ nextLineBreak = previousLineBreak;
}
-
- var glyphCount = 0;
-
- var currentWidth = 0.0;
-
- if (glyphRun.GlyphAdvances.IsEmpty)
+ else
{
- var glyphTypeface = glyphRun.GlyphTypeface;
+ shapedRuns = ShapeTextRuns(textRuns, paragraphProperties.FlowDirection,out flowDirection);
- if (glyphRun.IsLeftToRight)
+ if(nextLineBreak == null && textEndOfLine != null)
{
- foreach (var glyph in glyphRun.GlyphIndices)
- {
- var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
-
- if (currentWidth + advance > availableWidth)
- {
- break;
- }
-
- currentWidth += advance;
-
- glyphCount++;
- }
+ nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
}
- else
- {
- for (var index = glyphRun.GlyphClusters.Length - 1; index > 0; index--)
- {
- var glyph = glyphRun.GlyphIndices[index];
-
- var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
-
- if (currentWidth + advance > availableWidth)
- {
- break;
- }
+ }
- currentWidth += advance;
+ TextLineImpl textLine;
- glyphCount++;
- }
- }
- }
- else
+ switch (textWrapping)
{
- if (glyphRun.IsLeftToRight)
- {
- for (var index = 0; index < glyphRun.GlyphAdvances.Length; index++)
+ case TextWrapping.NoWrap:
{
- var advance = glyphRun.GlyphAdvances[index];
-
- if (currentWidth + advance > availableWidth)
- {
- break;
- }
+ TextLineImpl.SortRuns(shapedRuns);
- currentWidth += advance;
+ textLine = new TextLineImpl(shapedRuns, textRange, paragraphWidth, paragraphProperties,
+ flowDirection, nextLineBreak);
- glyphCount++;
+ textLine.FinalizeLine();
+
+ break;
}
- }
- else
- {
- for (var index = glyphRun.GlyphAdvances.Length - 1; index > 0; index--)
+ case TextWrapping.WrapWithOverflow:
+ case TextWrapping.Wrap:
{
- var advance = glyphRun.GlyphAdvances[index];
-
- if (currentWidth + advance > availableWidth)
- {
- break;
- }
-
- currentWidth += advance;
-
- glyphCount++;
+ textLine = PerformTextWrapping(shapedRuns, textRange, paragraphWidth, paragraphProperties,
+ flowDirection, nextLineBreak);
+ break;
}
- }
- }
-
- if (glyphCount == 0)
- {
- count = 0;
-
- return false;
- }
-
- if (glyphCount == glyphRun.GlyphIndices.Length)
- {
- count = glyphRun.Characters.Length;
-
- return true;
- }
-
- if (glyphRun.GlyphClusters.IsEmpty)
- {
- count = glyphCount;
-
- return true;
- }
-
- var firstCluster = glyphRun.GlyphClusters[0];
-
- var lastCluster = glyphRun.GlyphClusters[glyphCount];
-
- if (glyphRun.IsLeftToRight)
- {
- count = lastCluster - firstCluster;
- }
- else
- {
- count = firstCluster - lastCluster;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(textWrapping));
}
-
- return count > 0;
+ return textLine;
}
///
@@ -185,7 +71,7 @@ internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, d
/// The text run's.
/// The length to split at.
/// The split text runs.
- internal static SplitTextRunsResult SplitTextRuns(List textRuns, int length)
+ internal static SplitResult> SplitShapedRuns(List textRuns, int length)
{
var currentLength = 0;
@@ -193,13 +79,13 @@ internal static SplitTextRunsResult SplitTextRuns(List tex
{
var currentRun = textRuns[i];
- if (currentLength + currentRun.GlyphRun.Characters.Length <= length)
+ if (currentLength + currentRun.Text.Length < length)
{
- currentLength += currentRun.GlyphRun.Characters.Length;
+ currentLength += currentRun.Text.Length;
continue;
}
- var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
+ var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
var first = new List(firstCount);
@@ -213,14 +99,14 @@ internal static SplitTextRunsResult SplitTextRuns(List tex
var secondCount = textRuns.Count - firstCount;
- if (currentLength + currentRun.GlyphRun.Characters.Length == length)
+ if (currentLength + currentRun.Text.Length == length)
{
- var second = new List(secondCount);
-
- var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
+ var second = secondCount > 0 ? new List(secondCount) : null;
- if (secondCount > 0)
+ if (second != null)
{
+ var offset = currentRun.Text.Length >= 1 ? 1 : 0;
+
for (var j = 0; j < secondCount; j++)
{
second.Add(textRuns[i + j + offset]);
@@ -229,7 +115,7 @@ internal static SplitTextRunsResult SplitTextRuns(List tex
first.Add(currentRun);
- return new SplitTextRunsResult(first, second);
+ return new SplitResult>(first, second);
}
else
{
@@ -243,120 +129,203 @@ internal static SplitTextRunsResult SplitTextRuns(List tex
second.Add(split.Second!);
- if (secondCount > 0)
+ for (var j = 1; j < secondCount; j++)
{
- for (var j = 1; j < secondCount; j++)
- {
- second.Add(textRuns[i + j]);
- }
+ second.Add(textRuns[i + j]);
}
- return new SplitTextRunsResult(first, second);
+ return new SplitResult>(first, second);
}
}
- return new SplitTextRunsResult(textRuns, null);
+ return new SplitResult>(textRuns, null);
}
///
- /// Fetches text runs.
+ /// Shape specified text runs with specified paragraph embedding.
///
- /// The text source.
- /// The first text source index.
- /// Previous line break. Can be null.
- /// Next line break. Can be null.
+ /// The text runs to shape.
+ /// The paragraph embedding level.
+ /// The resolved flow direction.
///
- /// The formatted text runs.
+ /// A list of shaped text characters.
///
- private static List FetchTextRuns(ITextSource textSource,
- int firstTextSourceIndex, TextLineBreak? previousLineBreak, out TextLineBreak? nextLineBreak)
+ private static List ShapeTextRuns(List textRuns,
+ FlowDirection flowDirection, out FlowDirection resolvedFlowDirection)
{
- nextLineBreak = default;
+ var shapedTextCharacters = new List();
- var currentLength = 0;
+ var biDiData = new BidiData((sbyte)flowDirection);
- var textRuns = new List();
+ foreach (var textRun in textRuns)
+ {
+ biDiData.Append(textRun.Text);
+ }
- if (previousLineBreak?.RemainingCharacters != null)
+ var biDi = BidiAlgorithm.Instance.Value!;
+
+ biDi.Process(biDiData);
+
+ var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes);
+
+ resolvedFlowDirection =
+ (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
+
+ foreach (var shapeableRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{
- for (var index = 0; index < previousLineBreak.RemainingCharacters.Count; index++)
+ for (var index = 0; index < shapeableRuns.Count; index++)
{
- var shapedCharacters = previousLineBreak.RemainingCharacters[index];
+ var currentRun = shapeableRuns[index];
+
+ var shapedBuffer = TextShaper.Current.ShapeText(currentRun.Text, currentRun.Properties.Typeface.GlyphTypeface,
+ currentRun.Properties.FontRenderingEmSize, currentRun.Properties.CultureInfo, currentRun.BidiLevel);
+
+ var shapedCharacters = new ShapedTextCharacters(shapedBuffer, currentRun.Properties);
+
+
+ shapedTextCharacters.Add(shapedCharacters);
+ }
+ }
+
+ return shapedTextCharacters;
+ }
+
+ ///
+ /// Coalesces ranges of the same bidi level to form
+ ///
+ /// The text characters to form from.
+ /// The bidi levels.
+ ///
+ private static IEnumerable> CoalesceLevels(
+ IReadOnlyList textCharacters,
+ ReadOnlySlice levels)
+ {
+ if (levels.Length == 0)
+ {
+ yield break;
+ }
+
+ var levelIndex = 0;
+ var runLevel = levels[0];
- textRuns.Add(shapedCharacters);
+ TextRunProperties? previousProperties = null;
+ TextCharacters? currentRun = null;
+ var runText = ReadOnlySlice.Empty;
- if (TryGetLineBreak(shapedCharacters, out var runLineBreak))
+ for (var i = 0; i < textCharacters.Count; i++)
+ {
+ var j = 0;
+ currentRun = textCharacters[i];
+ runText = currentRun.Text;
+
+ for (; j < runText.Length;)
+ {
+ Codepoint.ReadAt(runText, j, out var count);
+
+ if (levelIndex + 1 == levels.Length)
{
- var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
+ break;
+ }
- if (splitResult.Second == null)
- {
- return splitResult.First;
- }
+ levelIndex++;
+ j += count;
- if (++index < previousLineBreak.RemainingCharacters.Count)
- {
- for (; index < previousLineBreak.RemainingCharacters.Count; index++)
- {
- splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]);
- }
- }
+ if (j == runText.Length)
+ {
+ yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
- nextLineBreak = new TextLineBreak(splitResult.Second);
+ runLevel = levels[levelIndex];
- return splitResult.First;
+ continue;
+ }
+
+ if (levels[levelIndex] == runLevel)
+ {
+ continue;
}
- currentLength += shapedCharacters.Text.Length;
+ // End of this run
+ yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
+
+ runText = runText.Skip(j);
+
+ j = 0;
+
+ // Move to next run
+ runLevel = levels[levelIndex];
}
}
- firstTextSourceIndex += currentLength;
+ if (currentRun is null || runText.IsEmpty)
+ {
+ yield break;
+ }
+
+ yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties);
+ }
+
+ ///
+ /// Fetches text runs.
+ ///
+ /// The text source.
+ /// The first text source index.
+ ///
+ ///
+ ///
+ /// The formatted text runs.
+ ///
+ private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
+ out TextEndOfLine? endOfLine, out TextRange textRange)
+ {
+ var length = 0;
+
+ endOfLine = null;
+
+ var textRuns = new List();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
while (textRunEnumerator.MoveNext())
{
- var textRun = textRunEnumerator.Current!;
+ var textRun = textRunEnumerator.Current;
+
+ if(textRun == null)
+ {
+ break;
+ }
switch (textRun)
{
case TextCharacters textCharacters:
- {
- var shapeableRuns = textCharacters.GetShapeableCharacters();
-
- foreach (var run in shapeableRuns)
{
- var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
- run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
+ if (TryGetLineBreak(textCharacters, out var runLineBreak))
+ {
+ var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
+ textCharacters.Properties);
- var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
+ textRuns.Add(splitResult);
- textRuns.Add(shapedCharacters);
- }
+ length += runLineBreak.PositionWrap;
- break;
- }
- case TextEndOfLine textEndOfLine:
- nextLineBreak = new TextLineBreak(textEndOfLine);
- break;
- }
+ textRange = new TextRange(firstTextSourceIndex, length);
- if (TryGetLineBreak(textRun, out var runLineBreak))
- {
- var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
+ return textRuns;
+ }
- if (splitResult.Second != null)
- {
- nextLineBreak = new TextLineBreak(splitResult.Second);
- }
+ textRuns.Add(textCharacters);
- return splitResult.First;
+ break;
+ }
+ case TextEndOfLine textEndOfLine:
+ endOfLine = textEndOfLine;
+ break;
}
- currentLength += textRun.Text.Length;
+ length += textRun.Text.Length;
}
+ textRange = new TextRange(firstTextSourceIndex, length);
+
return textRuns;
}
@@ -380,49 +349,52 @@ private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak)
lineBreak = lineBreakEnumerator.Current;
- if (lineBreak.PositionWrap >= textRun.Text.Length)
- {
- return true;
- }
-
- return true;
+ return lineBreak.PositionWrap >= textRun.Text.Length || true;
}
return false;
}
- ///
- /// Performs text wrapping returns a list of text lines.
- ///
- /// The text run's.
- /// The text range that is covered by the text runs.
- /// The paragraph width.
- /// The text paragraph properties.
- /// The current line break if the line was explicitly broken.
- /// The wrapped text line.
- private static TextLine PerformTextWrapping(List textRuns, TextRange textRange,
- double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? currentLineBreak)
+ private static int MeasureLength(IReadOnlyList textRuns, TextRange textRange,
+ double paragraphWidth)
{
- var availableWidth = paragraphWidth;
var currentWidth = 0.0;
- var measuredLength = 0;
+ var lastCluster = textRange.Start;
foreach (var currentRun in textRuns)
{
- if (currentWidth + currentRun.Size.Width > availableWidth)
+ for (var i = 0; i < currentRun.ShapedBuffer.Length; i++)
{
- if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var count))
+ var glyphInfo = currentRun.ShapedBuffer[i];
+
+ if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
- measuredLength += count;
+ return lastCluster - textRange.Start;
}
- break;
+ lastCluster = glyphInfo.GlyphCluster;
+ currentWidth += glyphInfo.GlyphAdvance;
}
+ }
- currentWidth += currentRun.Size.Width;
+ return textRange.Length;
+ }
- measuredLength += currentRun.Text.Length;
- }
+ ///
+ /// Performs text wrapping returns a list of text lines.
+ ///
+ ///
+ /// The text range that is covered by the text runs.
+ /// The paragraph width.
+ /// The text paragraph properties.
+ ///
+ /// The current line break if the line was explicitly broken.
+ /// The wrapped text line.
+ private static TextLineImpl PerformTextWrapping(List textRuns, TextRange textRange,
+ double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
+ TextLineBreak? currentLineBreak)
+ {
+ var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
var currentLength = 0;
@@ -430,154 +402,136 @@ private static TextLine PerformTextWrapping(List textRuns,
var currentPosition = 0;
- if (measuredLength == 0 && paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow)
+ for (var index = 0; index < textRuns.Count; index++)
{
- measuredLength = 1;
- }
- else
- {
- for (var index = 0; index < textRuns.Count; index++)
- {
- var currentRun = textRuns[index];
+ var currentRun = textRuns[index];
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
- var breakFound = false;
+ var breakFound = false;
- while (lineBreaker.MoveNext())
+ while (lineBreaker.MoveNext())
+ {
+ if (lineBreaker.Current.Required &&
+ currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
{
- if (lineBreaker.Current.Required &&
- currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
- {
- breakFound = true;
+ //Explicit break found
+ breakFound = true;
- currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
- break;
- }
+ break;
+ }
- if ((paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow || lastWrapPosition != 0) &&
- currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
+ if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
+ {
+ if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{
if (lastWrapPosition > 0)
{
currentPosition = lastWrapPosition;
- }
- else
- {
- currentPosition = currentLength + measuredLength;
- }
- breakFound = true;
+ breakFound = true;
- break;
- }
-
- if (currentLength + lineBreaker.Current.PositionWrap >= measuredLength)
- {
- currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ break;
+ }
- if (index < textRuns.Count - 1 &&
- lineBreaker.Current.PositionWrap == currentRun.Text.Length)
+ //Find next possible wrap position (overflow)
+ if (index < textRuns.Count - 1)
{
- var nextRun = textRuns[index + 1];
+ if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
+ {
+ //We already found the next possible wrap position.
+ breakFound = true;
- lineBreaker = new LineBreakEnumerator(nextRun.Text);
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+
+ break;
+ }
- if (lineBreaker.MoveNext() &&
- lineBreaker.Current.PositionMeasure == 0)
+ while (lineBreaker.MoveNext() && index < textRuns.Count)
{
currentPosition += lineBreaker.Current.PositionWrap;
+
+ if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
+ {
+ break;
+ }
+
+ index++;
+
+ if (index >= textRuns.Count)
+ {
+ break;
+ }
+
+ currentRun = textRuns[index];
+
+ lineBreaker = new LineBreakEnumerator(currentRun.Text);
}
}
+ else
+ {
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ }
breakFound = true;
break;
}
- lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
- }
+ //We overflowed so we use the last available wrap position.
+ currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
- if (!breakFound)
- {
- currentLength += currentRun.Text.Length;
+ breakFound = true;
- continue;
+ break;
}
- measuredLength = currentPosition;
-
- break;
+ if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
+ {
+ lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
+ }
}
- }
-
- var splitResult = SplitTextRuns(textRuns, measuredLength);
- textRange = new TextRange(textRange.Start, measuredLength);
+ if (!breakFound)
+ {
+ currentLength += currentRun.Text.Length;
- var remainingCharacters = splitResult.Second;
+ continue;
+ }
- var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null;
+ measuredLength = currentPosition;
- if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
- {
- lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine);
+ break;
}
- return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak);
- }
-
- ///
- /// Gets the text range that is covered by the text runs.
- ///
- /// The text runs.
- /// The text range that is covered by the text runs.
- private static TextRange GetTextRange(IReadOnlyList textRuns)
- {
- if (textRuns.Count == 0)
+ if (measuredLength == 0)
{
- return new TextRange();
+ measuredLength = 1;
}
- var firstTextRun = textRuns[0];
+ var splitResult = SplitShapedRuns(textRuns, measuredLength);
- if (textRuns.Count == 1)
- {
- return new TextRange(firstTextRun.Text.Start, firstTextRun.Text.Length);
- }
+ textRange = new TextRange(textRange.Start, measuredLength);
- var start = firstTextRun.Text.Start;
+ var remainingCharacters = splitResult.Second;
- var end = textRuns[textRuns.Count - 1].Text.End + 1;
+ var lineBreak = remainingCharacters?.Count > 0 ?
+ new TextLineBreak(currentLineBreak?.TextEndOfLine, flowDirection, remainingCharacters) :
+ null;
- return new TextRange(start, end - start);
- }
-
- internal readonly struct SplitTextRunsResult
- {
- public SplitTextRunsResult(List first, List? second)
+ if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
{
- First = first;
-
- Second = second;
+ lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
}
- ///
- /// Gets the first text runs.
- ///
- ///
- /// The first text runs.
- ///
- public List First { get; }
-
- ///
- /// Gets the second text runs.
- ///
- ///
- /// The second text runs.
- ///
- public List? Second { get; }
+ TextLineImpl.SortRuns(splitResult.First);
+
+ var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection,
+ lineBreak);
+
+ return textLine.FinalizeLine();
}
private struct TextRunEnumerator
@@ -614,5 +568,28 @@ public bool MoveNext()
return true;
}
}
+
+ ///
+ /// Creates a shaped symbol.
+ ///
+ /// The symbol run to shape.
+ /// The flow direction.
+ ///
+ /// The shaped symbol.
+ ///
+ internal static ShapedTextCharacters CreateSymbol(TextRun textRun, FlowDirection flowDirection)
+ {
+ var textShaper = TextShaper.Current;
+
+ var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface;
+
+ var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
+
+ var cultureInfo = textRun.Properties.CultureInfo;
+
+ var shapedBuffer = textShaper.ShapeText(textRun.Text, glyphTypeface, fontRenderingEmSize, cultureInfo, (sbyte)flowDirection);
+
+ return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 0ed06e4e575..f01ef886f73 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Utilities;
@@ -11,7 +10,7 @@ namespace Avalonia.Media.TextFormatting
///
public class TextLayout
{
- private static readonly char[] s_empty = { '\u200B' };
+ private static readonly char[] s_empty = { ' ' };
private readonly ReadOnlySlice _text;
private readonly TextParagraphProperties _paragraphProperties;
@@ -29,6 +28,7 @@ public class TextLayout
/// The text wrapping.
/// The text trimming.
/// The text decorations.
+ /// The text flow direction.
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
@@ -43,6 +43,7 @@ public TextLayout(
TextWrapping textWrapping = TextWrapping.NoWrap,
TextTrimming textTrimming = TextTrimming.None,
TextDecorationCollection? textDecorations = null,
+ FlowDirection flowDirection = FlowDirection.LeftToRight,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
@@ -55,7 +56,7 @@ public TextLayout(
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
- textDecorations, lineHeight);
+ textDecorations, flowDirection, lineHeight);
_textTrimming = textTrimming;
@@ -69,7 +70,7 @@ public TextLayout(
MaxLines = maxLines;
- UpdateLayout();
+ TextLines = CreateTextLines();
}
///
@@ -190,29 +191,174 @@ public IEnumerable HitTestTextRange(int start, int length)
}
var result = new List(TextLines.Count);
-
+
var currentY = 0d;
+ var currentPosition = 0;
foreach (var textLine in TextLines)
{
- var currentX = textLine.Start;
-
- if (textLine.TextRange.End < start)
+ //Current line isn't covered.
+ if (currentPosition + textLine.TextRange.Length <= start)
{
currentY += textLine.Height;
+ currentPosition += textLine.TextRange.Length;
continue;
}
- if (start > textLine.TextRange.Start)
+ //The whole line is covered.
+ if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length)
{
- currentX += textLine.GetDistanceFromCharacterHit(new CharacterHit(start));
+ result.Add(new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace, textLine.Height));
+
+ currentY += textLine.Height;
+ currentPosition += textLine.TextRange.Length;
+
+ continue;
}
+
+ var startX = textLine.Start;
+
+ //A portion of the line is covered.
+ for (var index = 0; index < textLine.TextRuns.Count; index++)
+ {
+ var currentRun = (ShapedTextCharacters)textLine.TextRuns[index];
+ ShapedTextCharacters? nextRun = null;
+
+ if (index + 1 < textLine.TextRuns.Count)
+ {
+ nextRun = (ShapedTextCharacters)textLine.TextRuns[index + 1];
+ }
+
+ if (nextRun != null)
+ {
+ if (nextRun.Text.Start < currentRun.Text.Start && start + length < currentRun.Text.End)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.Start >= start + length)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < start)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.End < start)
+ {
+ goto skip;
+ }
+
+ goto noop;
+
+ skip:
+ {
+ startX += currentRun.Size.Width;
+
+ currentPosition = currentRun.Text.Start;
+ }
+
+ continue;
+
+ noop:{ }
+ }
+
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
+ currentRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(start + length) :
+ new CharacterHit(start));
+
+ var endX = startX + endOffset;
+
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
+ currentRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(start) :
+ new CharacterHit(start + length));
+
+ startX += startOffset;
+
+ var characterHit = currentRun.GlyphRun.IsLeftToRight ?
+ currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
+ currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ if(nextRun != null)
+ {
+ if (currentRun.ShapedBuffer.IsLeftToRight == nextRun.ShapedBuffer.IsLeftToRight)
+ {
+ endOffset = nextRun.GlyphRun.GetDistanceFromCharacterHit(
+ nextRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(start + length) :
+ new CharacterHit(start));
+
+ index++;
+
+ endX += endOffset;
+
+ currentRun = nextRun;
+
+ if (currentRun.ShapedBuffer.IsLeftToRight)
+ {
+ characterHit = nextRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ }
+ }
+ }
- var endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length));
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
+ }
+
+ var width = endX - startX;
- result.Add(new Rect(currentX, currentY, endX - currentX, textLine.Height));
+ result.Add(new Rect(startX, currentY, width, textLine.Height));
+ if (currentRun.ShapedBuffer.IsLeftToRight)
+ {
+ if (nextRun != null)
+ {
+ if (nextRun.Text.Start > currentRun.Text.Start)
+ {
+ break;
+ }
+
+ currentPosition = nextRun.Text.End;
+ }
+ else
+ {
+ if (currentPosition >= start + length)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ if (currentPosition <= start)
+ {
+ break;
+ }
+ }
+
+ if (!currentRun.ShapedBuffer.IsLeftToRight && currentPosition != currentRun.Text.Start)
+ {
+ endX += currentRun.GlyphRun.Size.Width - endOffset;
+ }
+
+ startX = endX;
+ }
+
+ if (currentPosition == start || currentPosition == start + length)
+ {
+ break;
+ }
+
if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
{
break;
@@ -256,6 +402,37 @@ public TextHitTestResult HitTestPoint(in Point point)
return GetHitTestResult(currentLine, characterHit, point);
}
+
+ public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
+ {
+ if (charIndex < 0)
+ {
+ return 0;
+ }
+
+ if (charIndex > _text.Length)
+ {
+ return TextLines.Count - 1;
+ }
+
+ for (var index = 0; index < TextLines.Count; index++)
+ {
+ var textLine = TextLines[index];
+
+ if (textLine.TextRange.Start + textLine.TextRange.Length < charIndex)
+ {
+ continue;
+ }
+
+ if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0))
+ {
+ return index;
+ }
+ }
+
+ return TextLines.Count - 1;
+ }
+
private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
{
var (x, y) = point;
@@ -274,7 +451,18 @@ private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit chara
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Size.Height;
- return new TextHitTestResult { IsInside = isInside, IsTrailing = isTrailing, TextPosition = textPosition };
+ if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length)
+ {
+ textPosition -= textLine.NewLineLength;
+ }
+
+ if (textLine.NewLineLength > 0 && textPosition + textLine.NewLineLength ==
+ characterHit.FirstCharacterIndex + characterHit.TrailingLength)
+ {
+ characterHit = new CharacterHit(characterHit.FirstCharacterIndex);
+ }
+
+ return new TextHitTestResult(characterHit, textPosition, isInside, isTrailing);
}
///
@@ -286,15 +474,16 @@ private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit chara
/// The text alignment.
/// The text wrapping.
/// The text decorations.
+ /// The text flow direction.
/// The height of each line of text.
///
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping,
- TextDecorationCollection? textDecorations, double lineHeight)
+ TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
- return new GenericTextParagraphProperties(FlowDirection.LeftToRight, textAlignment, true, false,
+ return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0);
}
@@ -306,8 +495,8 @@ private static TextParagraphProperties CreateTextParagraphProperties(Typeface ty
/// The current height.
private static void UpdateBounds(TextLine textLine, ref double width, ref double height)
{
- var lineWidth = textLine.Width + textLine.Start * 2;
-
+ var lineWidth = textLine.WidthIncludingTrailingWhitespace + textLine.Start * 2;
+
if (width < lineWidth)
{
width = lineWidth;
@@ -322,97 +511,97 @@ private static void UpdateBounds(TextLine textLine, ref double width, ref double
/// The empty text line.
private TextLine CreateEmptyTextLine(int startingIndex)
{
+ var flowDirection = _paragraphProperties.FlowDirection;
var properties = _paragraphProperties.DefaultTextRunProperties;
+ var glyphTypeface = properties.Typeface.GlyphTypeface;
+ var text = new ReadOnlySlice(s_empty, startingIndex, 1);
+ var glyph = glyphTypeface.GetGlyph(s_empty[0]);
+ var glyphInfos = new[] { new GlyphInfo(glyph, startingIndex) };
- var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice(s_empty, startingIndex, 1),
- properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo);
+ var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
+ (sbyte)flowDirection);
- var textRuns = new List
- {
- new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties)
- };
+ var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
var textRange = new TextRange(startingIndex, 1);
- return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties);
+ return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
}
- ///
- /// Updates the layout and applies specified text style overrides.
- ///
- [MemberNotNull(nameof(TextLines))]
- private void UpdateLayout()
+ private IReadOnlyList CreateTextLines()
{
if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = CreateEmptyTextLine(0);
- TextLines = new List { textLine };
-
Size = new Size(0, textLine.Height);
+
+ return new List { textLine };
}
- else
- {
- var textLines = new List();
- double width = 0.0, height = 0.0;
+ var textLines = new List();
- var currentPosition = 0;
+ double width = 0.0, height = 0.0;
- var textSource = new FormattedTextSource(_text,
- _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
+ var currentPosition = 0;
- TextLine? previousLine = null;
+ var textSource = new FormattedTextSource(_text,
+ _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
- while (currentPosition < _text.Length)
- {
- var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
- _paragraphProperties, previousLine?.TextLineBreak);
+ TextLine? previousLine = null;
- currentPosition += textLine.TextRange.Length;
+ while (currentPosition < _text.Length)
+ {
+ var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
+ _paragraphProperties, previousLine?.TextLineBreak);
- if (textLines.Count > 0)
+ currentPosition += textLine.TextRange.Length;
+
+ if (textLines.Count > 0)
+ {
+ if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
+ height + textLine.Height > MaxHeight)
{
- if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
- height + textLine.Height > MaxHeight)
+ if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
- if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
- {
- var collapsedLine =
- previousLine.Collapse(GetCollapsingProperties(MaxWidth));
-
- textLines[textLines.Count - 1] = collapsedLine;
- }
+ var collapsedLine =
+ previousLine.Collapse(GetCollapsingProperties(MaxWidth));
- break;
+ textLines[textLines.Count - 1] = collapsedLine;
}
- }
- var hasOverflowed = textLine.HasOverflowed;
-
- if (hasOverflowed && _textTrimming != TextTrimming.None)
- {
- textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
+ break;
}
+ }
- textLines.Add(textLine);
+ var hasOverflowed = textLine.HasOverflowed;
- UpdateBounds(textLine, ref width, ref height);
+ if (hasOverflowed && _textTrimming != TextTrimming.None)
+ {
+ textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
+ }
- previousLine = textLine;
+ textLines.Add(textLine);
- if (currentPosition == _text.Length && textLine.NewLineLength > 0)
- {
- var emptyTextLine = CreateEmptyTextLine(currentPosition);
+ UpdateBounds(textLine, ref width, ref height);
- textLines.Add(emptyTextLine);
- }
+ previousLine = textLine;
+
+ if (currentPosition != _text.Length || textLine.NewLineLength <= 0)
+ {
+ continue;
}
- Size = new Size(width, height);
+ var emptyTextLine = CreateEmptyTextLine(currentPosition);
- TextLines = textLines;
+ textLines.Add(emptyTextLine);
+
+ UpdateBounds(emptyTextLine, ref width, ref height);
}
+
+ Size = new Size(width, height);
+
+ return textLines;
}
///
@@ -431,241 +620,5 @@ private TextCollapsingProperties GetCollapsingProperties(double width)
_ => throw new ArgumentOutOfRangeException(),
};
}
-
- public int GetLineIndexFromCharacterIndex(int charIndex)
- {
- if (TextLines is null)
- {
- return -1;
- }
-
- if (charIndex < 0)
- {
- return -1;
- }
-
- if (charIndex > _text.Length - 1)
- {
- return TextLines.Count - 1;
- }
-
- for (var index = 0; index < TextLines.Count; index++)
- {
- var textLine = TextLines[index];
-
- if (textLine.TextRange.End < charIndex)
- {
- continue;
- }
-
- if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End)
- {
- return index;
- }
- }
-
- return TextLines.Count - 1;
- }
-
- public int GetCharacterIndexFromPoint(Point point, bool snapToText)
- {
- if (TextLines is null)
- {
- return -1;
- }
-
- var (x, y) = point;
-
- if (!snapToText && y > Size.Height)
- {
- return -1;
- }
-
- var currentY = 0d;
-
- foreach (var textLine in TextLines)
- {
- if (currentY + textLine.Height <= y)
- {
- currentY += textLine.Height;
-
- continue;
- }
-
- if (x > textLine.WidthIncludingTrailingWhitespace)
- {
- if (snapToText)
- {
- return textLine.TextRange.End;
- }
-
- return -1;
- }
-
- var characterHit = textLine.GetCharacterHitFromDistance(x);
-
- return characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- }
-
- return _text.Length;
- }
-
- public Rect GetRectFromCharacterIndex(int characterIndex, bool trailingEdge)
- {
- if (TextLines is null)
- {
- return Rect.Empty;
- }
-
- var distanceY = 0d;
-
- var currentIndex = 0;
-
- foreach (var textLine in TextLines)
- {
- if (currentIndex + textLine.TextRange.Length < characterIndex)
- {
- distanceY += textLine.Height;
-
- currentIndex += textLine.TextRange.Length;
-
- continue;
- }
-
- var characterHit = new CharacterHit(characterIndex);
-
- while (characterHit.FirstCharacterIndex < characterIndex)
- {
- characterHit = textLine.GetNextCaretCharacterHit(characterHit);
- }
-
- var distanceX = textLine.GetDistanceFromCharacterHit(trailingEdge ?
- characterHit :
- new CharacterHit(characterHit.FirstCharacterIndex));
-
- if (characterHit.TrailingLength > 0)
- {
- distanceX += 1;
- }
-
- return new Rect(distanceX, distanceY, 0, textLine.Height);
- }
-
- return Rect.Empty;
- }
-
- private readonly struct FormattedTextSource : ITextSource
- {
- private readonly ReadOnlySlice _text;
- private readonly TextRunProperties _defaultProperties;
- private readonly IReadOnlyList>? _textModifier;
-
- public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties,
- IReadOnlyList>? textModifier)
- {
- _text = text;
- _defaultProperties = defaultProperties;
- _textModifier = textModifier;
- }
-
- public TextRun? GetTextRun(int textSourceIndex)
- {
- if (textSourceIndex > _text.Length)
- {
- return null;
- }
-
- var runText = _text.Skip(textSourceIndex);
-
- if (runText.IsEmpty)
- {
- return new TextEndOfParagraph();
- }
-
- var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
-
- return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
- }
-
- ///
- /// Creates a span of text run properties that has modifier applied.
- ///
- /// The text to create the properties for.
- /// The default text properties.
- /// The text properties modifier.
- ///
- /// The created text style run.
- ///
- private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
- TextRunProperties defaultProperties, IReadOnlyList>? textModifier)
- {
- if (textModifier == null || textModifier.Count == 0)
- {
- return new ValueSpan(text.Start, text.Length, defaultProperties);
- }
-
- var currentProperties = defaultProperties;
-
- var hasOverride = false;
-
- var i = 0;
-
- var length = 0;
-
- for (; i < textModifier.Count; i++)
- {
- var propertiesOverride = textModifier[i];
-
- var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
-
- if (textRange.End < text.Start)
- {
- continue;
- }
-
- if (textRange.Start > text.End)
- {
- length = text.Length;
- break;
- }
-
- if (textRange.Start > text.Start)
- {
- if (propertiesOverride.Value != currentProperties)
- {
- length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
-
- break;
- }
- }
-
- length += Math.Min(text.Length - length, textRange.Length);
-
- if (hasOverride)
- {
- continue;
- }
-
- hasOverride = true;
-
- currentProperties = propertiesOverride.Value;
- }
-
- if (length < text.Length && i == textModifier.Count)
- {
- if (currentProperties == defaultProperties)
- {
- length = text.Length;
- }
- }
-
- if (length != text.Length)
- {
- text = text.Take(length);
- }
-
- return new ValueSpan(text.Start, length, currentProperties);
- }
- }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index aea42270021..9bbc4a8a9dd 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -191,27 +191,45 @@ public abstract class TextLine
///
/// Gets the text line offset x.
///
- /// The line width.
+ /// The line width.
+ /// The paragraph width including whitespace.
/// The paragraph width.
/// The text alignment.
+ /// The flow direction of the line.
/// The paragraph offset.
- internal static double GetParagraphOffsetX(double lineWidth, double paragraphWidth, TextAlignment textAlignment)
+ internal static double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace,
+ double paragraphWidth, TextAlignment textAlignment, FlowDirection flowDirection)
{
if (double.IsPositiveInfinity(paragraphWidth))
{
return 0;
}
+ if (flowDirection == FlowDirection.LeftToRight)
+ {
+ switch (textAlignment)
+ {
+ case TextAlignment.Center:
+ return (paragraphWidth - width) / 2;
+
+ case TextAlignment.Right:
+ return paragraphWidth - widthIncludingTrailingWhitespace;
+
+ default:
+ return 0;
+ }
+ }
+
switch (textAlignment)
{
case TextAlignment.Center:
- return (paragraphWidth - lineWidth) / 2;
+ return (paragraphWidth - width) / 2;
case TextAlignment.Right:
- return paragraphWidth - lineWidth;
+ return 0;
default:
- return 0.0f;
+ return paragraphWidth - widthIncludingTrailingWhitespace;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
index d2bd58682ae..be9661c2bfe 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
@@ -4,21 +4,24 @@ namespace Avalonia.Media.TextFormatting
{
public class TextLineBreak
{
- public TextLineBreak(TextEndOfLine textEndOfLine)
+ public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight,
+ IReadOnlyList? remainingCharacters = null)
{
TextEndOfLine = textEndOfLine;
- }
-
- public TextLineBreak(IReadOnlyList remainingCharacters)
- {
+ FlowDirection = flowDirection;
RemainingCharacters = remainingCharacters;
}
-
+
///
- /// Get the
+ /// Get the end of line run.
///
public TextEndOfLine? TextEndOfLine { get; }
+ ///
+ /// Get the flow direction for remaining characters.
+ ///
+ public FlowDirection FlowDirection { get; }
+
///
/// Get the remaining shaped characters that were split up by the during the formatting process.
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index b1397518e4d..53e44de779e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -1,34 +1,40 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
- private readonly List _shapedTextRuns;
+ private static readonly Comparer s_compareStart = Comparer.Default;
+
+ private static readonly Comparison s_compareLogicalOrder =
+ (a, b) => s_compareStart.Compare(a.Text.Start, b.Text.Start);
+
+ private readonly List _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
- private readonly TextLineMetrics _textLineMetrics;
+ private TextLineMetrics _textLineMetrics;
+ private readonly FlowDirection _flowDirection;
public TextLineImpl(List textRuns, TextRange textRange, double paragraphWidth,
- TextParagraphProperties paragraphProperties, TextLineBreak? lineBreak = null, bool hasCollapsed = false)
+ TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
+ TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{
TextRange = textRange;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
- _shapedTextRuns = textRuns;
+ _textRuns = textRuns;
_paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties;
- _textLineMetrics = CreateLineMetrics();
+ _flowDirection = flowDirection;
}
///
- public override IReadOnlyList TextRuns => _shapedTextRuns;
+ public override IReadOnlyList TextRuns => _textRuns;
///
public override TextRange TextRange { get; }
@@ -80,12 +86,12 @@ public override void Draw(DrawingContext drawingContext, Point lineOrigin)
{
var (currentX, currentY) = lineOrigin;
- foreach (var textRun in _shapedTextRuns)
+ foreach (var textRun in _textRuns)
{
var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
-
+
currentX += textRun.Size.Width;
}
}
@@ -93,7 +99,7 @@ public override void Draw(DrawingContext drawingContext, Point lineOrigin)
///
public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
{
- if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0)
+ if (collapsingPropertiesList.Length == 0)
{
return this;
}
@@ -105,21 +111,22 @@ public override TextLine Collapse(params TextCollapsingProperties[] collapsingPr
var textRange = TextRange;
var collapsedLength = 0;
- var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
+ var shapedSymbol = TextFormatterImpl.CreateSymbol(collapsingProperties.Symbol, _paragraphProperties.FlowDirection);
- var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width;
+ var availableWidth = collapsingProperties.Width - shapedSymbol.GlyphRun.Size.Width;
- while (runIndex < _shapedTextRuns.Count)
+ while (runIndex < _textRuns.Count)
{
- var currentRun = _shapedTextRuns[runIndex];
+ var currentRun = _textRuns[runIndex];
currentWidth += currentRun.Size.Width;
if (currentWidth > availableWidth)
{
- if (TextFormatterImpl.TryMeasureCharacters(currentRun, availableWidth, out var measuredLength))
+ if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
- if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord && measuredLength < textRange.End)
+ if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord &&
+ measuredLength < textRange.End)
{
var currentBreakPosition = 0;
@@ -148,18 +155,22 @@ public override TextLine Collapse(params TextCollapsingProperties[] collapsingPr
collapsedLength += measuredLength;
- var splitResult = TextFormatterImpl.SplitTextRuns(_shapedTextRuns, collapsedLength);
+ var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength);
var shapedTextCharacters = new List(splitResult.First.Count + 1);
shapedTextCharacters.AddRange(splitResult.First);
+ SortRuns(shapedTextCharacters);
+
shapedTextCharacters.Add(shapedSymbol);
textRange = new TextRange(textRange.Start, collapsedLength);
- return new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
- TextLineBreak, true);
+ var textLine = new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
+ _flowDirection, TextLineBreak, true);
+
+ return textLine.FinalizeLine();
}
availableWidth -= currentRun.Size.Width;
@@ -172,78 +183,23 @@ public override TextLine Collapse(params TextCollapsingProperties[] collapsingPr
return this;
}
- private TextLineMetrics CreateLineMetrics()
- {
- var width = 0d;
- var widthIncludingWhitespace = 0d;
- var trailingWhitespaceLength = 0;
- var newLineLength = 0;
- var ascent = 0d;
- var descent = 0d;
- var lineGap = 0d;
-
- for (var index = 0; index < _shapedTextRuns.Count; index++)
- {
- var textRun = _shapedTextRuns[index];
-
- var fontMetrics =
- new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
-
- if (ascent > fontMetrics.Ascent)
- {
- ascent = fontMetrics.Ascent;
- }
-
- if (descent < fontMetrics.Descent)
- {
- descent = fontMetrics.Descent;
- }
-
- if (lineGap < fontMetrics.LineGap)
- {
- lineGap = fontMetrics.LineGap;
- }
-
- if (index == _shapedTextRuns.Count - 1)
- {
- width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
- widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
- trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
- }
- else
- {
- widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
- }
- }
-
- var start = GetParagraphOffsetX(width, _paragraphWidth, _paragraphProperties.TextAlignment);
-
- var lineHeight = _paragraphProperties.LineHeight;
-
- var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
- descent - ascent + lineGap :
- lineHeight;
-
- return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
- -ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
- }
-
///
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
distance -= Start;
-
- if (distance < 0)
+
+ if (distance <= 0)
{
// hit happens before the line, return the first position
- return new CharacterHit(TextRange.Start);
+ var firstRun = _textRuns[0];
+
+ return firstRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
- foreach (var run in _shapedTextRuns)
+ foreach (var run in _textRuns)
{
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
@@ -263,27 +219,90 @@ public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
- if (characterIndex > TextRange.End)
+ var currentDistance = Start;
+
+ GlyphRun? lastRun = null;
+
+ for (var index = 0; index < _textRuns.Count; index++)
{
- if (NewLineLength > 0)
+ var textRun = _textRuns[index];
+ var currentRun = textRun.GlyphRun;
+
+ if (lastRun != null)
{
- return Start + Width;
+ if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
+ currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
+ characterHit.TrailingLength == 0)
+ {
+ return currentDistance;
+ }
}
- return Start + WidthIncludingTrailingWhitespace;
- }
- var currentDistance = Start;
+ //Look for a hit in within the current run
+ if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.End)
+ {
+ var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
- foreach (var textRun in _shapedTextRuns)
- {
- if (characterIndex > textRun.Text.End)
+ return currentDistance + distance;
+ }
+
+ //Look at the left and right edge of the current run
+ if (currentRun.IsLeftToRight)
{
- currentDistance += textRun.Size.Width;
+ if (lastRun == null || lastRun.IsLeftToRight)
+ {
+ if (characterIndex <= textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex == textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
- continue;
+ if (characterIndex == textRun.Text.Start + textRun.Text.Length && characterHit.TrailingLength > 0)
+ {
+ return currentDistance + currentRun.Size.Width;
+ }
}
+ else
+ {
+ if (characterIndex == textRun.Text.Start)
+ {
+ return currentDistance + currentRun.Size.Width;
+ }
+
+ var nextRun = index + 1 < _textRuns.Count ? _textRuns[index + 1] : null;
+
+ if (nextRun != null)
+ {
+ if (characterHit.FirstCharacterIndex == textRun.Text.End && nextRun.ShapedBuffer.IsLeftToRight)
+ {
+ return currentDistance;
+ }
- return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex));
+ if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex > textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ }
+
+ //No hit hit found so we add the full width
+ currentDistance += currentRun.Size.Width;
+
+ lastRun = currentRun;
}
return currentDistance;
@@ -297,18 +316,14 @@ public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
return nextCharacterHit;
}
- if (characterHit.FirstCharacterIndex + characterHit.TrailingLength <= TextRange.Start + TextRange.Length)
- {
- return characterHit; // Can't move, we're after the last character
- }
+ // Can't move, we're after the last character
+ var runIndex = GetRunIndexAtCharacterIndex(TextRange.End, LogicalDirection.Forward);
- var runIndex = GetRunIndexAtCodepointIndex(TextRange.End);
-
- var textRun = _shapedTextRuns[runIndex];
+ var textRun = _textRuns[runIndex];
characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
- return characterHit; // Can't move, we're after the last character
+ return characterHit;
}
///
@@ -319,7 +334,7 @@ public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit character
return previousCharacterHit;
}
- if (characterHit.FirstCharacterIndex < TextRange.Start)
+ if (characterHit.FirstCharacterIndex <= TextRange.Start)
{
characterHit = new CharacterHit(TextRange.Start);
}
@@ -334,6 +349,170 @@ public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characte
return GetPreviousCaretCharacterHit(characterHit);
}
+ public static void SortRuns(List textRuns)
+ {
+ textRuns.Sort(s_compareLogicalOrder);
+ }
+
+ public TextLineImpl FinalizeLine()
+ {
+ BidiReorder();
+
+ _textLineMetrics = CreateLineMetrics();
+
+ return this;
+ }
+
+ private void BidiReorder()
+ {
+ // Build up the collection of ordered runs.
+ var run = _textRuns[0];
+ OrderedBidiRun orderedRun = new(run);
+ var current = orderedRun;
+
+ for (var i = 1; i < _textRuns.Count; i++)
+ {
+ run = _textRuns[i];
+
+ current.Next = new OrderedBidiRun(run);
+
+ 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 level = _textRuns[i].BidiLevel;
+
+ 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.IsReversed)
+ {
+ current.Run.Reverse();
+ }
+ }
+
+ current = current.Next;
+ }
+
+ minLevelToReverse--;
+ }
+
+ _textRuns.Clear();
+
+ current = orderedRun;
+
+ while (current != null)
+ {
+ _textRuns.Add(current.Run);
+
+ current = current.Next;
+ }
+ }
+
+ ///
+ /// 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!;
+ }
+
///
/// Tries to find the next character hit.
///
@@ -346,7 +525,7 @@ private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- if (codepointIndex > TextRange.End)
+ if (codepointIndex >= TextRange.End)
{
return false; // Cannot go forward anymore
}
@@ -356,21 +535,29 @@ private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit
codepointIndex = TextRange.Start;
}
- var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
+ var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward);
- while (runIndex < _shapedTextRuns.Count)
+ while (runIndex < _textRuns.Count)
{
- var run = _shapedTextRuns[runIndex];
+ var run = _textRuns[runIndex];
var foundCharacterHit =
- run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+ run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength,
+ out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
TextRange.Length;
+ if (isAtEnd && !run.GlyphRun.IsLeftToRight)
+ {
+ nextCharacterHit = foundCharacterHit;
+
+ return true;
+ }
+
var characterIndex = codepointIndex - run.Text.Start;
- if (characterIndex < 0 && characterHit.TrailingLength == 0)
+ if (characterIndex < 0 && run.ShapedBuffer.IsLeftToRight)
{
foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
@@ -398,7 +585,9 @@ private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit
///
private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit)
{
- if (characterHit.FirstCharacterIndex == TextRange.Start)
+ var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ if (characterIndex == TextRange.Start)
{
previousCharacterHit = new CharacterHit(TextRange.Start);
@@ -407,26 +596,32 @@ private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out Characte
previousCharacterHit = characterHit;
- var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-
- if (codepointIndex < TextRange.Start)
+ if (characterIndex < TextRange.Start)
{
return false; // Cannot go backward anymore.
}
- var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
+ var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward);
while (runIndex >= 0)
{
- var run = _shapedTextRuns[runIndex];
+ var run = _textRuns[runIndex];
- var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
+ var foundCharacterHit =
+ run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
+ if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
+ {
+ previousCharacterHit = foundCharacterHit;
+
+ return true;
+ }
+
previousCharacterHit = characterHit.TrailingLength != 0 ?
foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex);
- if (previousCharacterHit.FirstCharacterIndex < characterHit.FirstCharacterIndex)
+ if (previousCharacterHit != characterHit)
{
return true;
}
@@ -441,52 +636,203 @@ private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out Characte
/// Gets the run index of the specified codepoint index.
///
/// The codepoint index.
+ /// The logical direction.
/// The text run index.
- private int GetRunIndexAtCodepointIndex(int codepointIndex)
+ private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction)
{
- if (codepointIndex > TextRange.End)
- {
- return _shapedTextRuns.Count - 1;
- }
+ var runIndex = 0;
+ ShapedTextCharacters? previousRun = null;
- if (codepointIndex <= 0)
+ while (runIndex < _textRuns.Count)
{
- return 0;
+ var currentRun = _textRuns[runIndex];
+
+ if (previousRun != null && !previousRun.ShapedBuffer.IsLeftToRight)
+ {
+ if (currentRun.ShapedBuffer.IsLeftToRight)
+ {
+ if (currentRun.Text.Start >= codepointIndex)
+ {
+ return --runIndex;
+ }
+ }
+ else
+ {
+ if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+ {
+ return --runIndex;
+ }
+ }
+ }
+
+ if (direction == LogicalDirection.Forward)
+ {
+ if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
+ {
+ return runIndex;
+ }
+ }
+ else
+ {
+ if (codepointIndex > currentRun.Text.Start &&
+ codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
+ {
+ return runIndex;
+ }
+ }
+
+ if (runIndex + 1 < _textRuns.Count)
+ {
+ runIndex++;
+ previousRun = currentRun;
+ }
+ else
+ {
+ break;
+ }
}
- var runIndex = 0;
+ return runIndex;
+ }
- while (runIndex < _shapedTextRuns.Count)
+ private TextLineMetrics CreateLineMetrics()
+ {
+ var width = 0d;
+ var widthIncludingWhitespace = 0d;
+ var trailingWhitespaceLength = 0;
+ var newLineLength = 0;
+ var ascent = 0d;
+ var descent = 0d;
+ var lineGap = 0d;
+ var fontRenderingEmSize = 0d;
+
+ for (var index = 0; index < _textRuns.Count; index++)
{
- var run = _shapedTextRuns[runIndex];
+ var textRun = _textRuns[index];
- if (run.Text.End >= codepointIndex)
+ var fontMetrics =
+ new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+
+ if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
{
- return runIndex;
+ fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
+
+ if (ascent > fontMetrics.Ascent)
+ {
+ ascent = fontMetrics.Ascent;
+ }
+
+ if (descent < fontMetrics.Descent)
+ {
+ descent = fontMetrics.Descent;
+ }
+
+ if (lineGap < fontMetrics.LineGap)
+ {
+ lineGap = fontMetrics.LineGap;
+ }
}
- runIndex++;
+ switch (_paragraphProperties.FlowDirection)
+ {
+ case FlowDirection.LeftToRight:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
+ trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ }
+
+ break;
+ }
+
+ case FlowDirection.RightToLeft:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ var firstRun = _textRuns[0];
+
+ var offset = firstRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
+ firstRun.GlyphRun.Metrics.Width;
+
+ width = widthIncludingWhitespace +
+ textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
+
+ trailingWhitespaceLength = firstRun.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = firstRun.GlyphRun.Metrics.NewlineLength;
+ }
+
+ break;
+ }
+ }
+
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
}
- return runIndex;
+ var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
+ _paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
+
+ var lineHeight = _paragraphProperties.LineHeight;
+
+ var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
+ descent - ascent + lineGap :
+ lineHeight;
+
+ return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
+ -ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
}
- ///
- /// Creates a shaped symbol.
- ///
- /// The symbol run to shape.
- ///
- /// The shaped symbol.
- ///
- internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun)
+ private sealed class OrderedBidiRun
{
- var properties = textRun.Properties;
+ public OrderedBidiRun(ShapedTextCharacters run) => Run = run;
+
+ public sbyte Level => Run.BidiLevel;
- _ = properties ?? throw new InvalidOperationException($"{nameof(TextRun.Properties)} should not be null.");
+ public ShapedTextCharacters Run { get; }
- var glyphRun = TextShaper.Current.ShapeText(textRun.Text, properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo);
+ public OrderedBidiRun? Next { get; set; }
- return new ShapedTextCharacters(glyphRun, properties);
+ public void Reverse() => Run.ShapedBuffer.GlyphInfos.Span.Reverse();
+ }
+
+ 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.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
index bbff09ad79f..b799567a603 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
@@ -7,7 +7,7 @@ public abstract class TextParagraphProperties
{
///
/// This property specifies whether the primary text advance
- /// direction shall be left-to-right, right-to-left, or top-to-bottom.
+ /// direction shall be left-to-right, right-to-left.
///
public abstract FlowDirection FlowDirection { get; }
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
index 4bfbb89006b..26c3f8947a4 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
@@ -41,7 +41,7 @@ public string Text
{
unsafe
{
- fixed (char* charsPtr = _textRun.Text.Buffer.Span)
+ fixed (char* charsPtr = _textRun.Text.Span)
{
return new string(charsPtr, 0, _textRun.Text.Length);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
index 2892e608ab3..c982a435c37 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
@@ -45,10 +45,10 @@ public static TextShaper Current
}
///
- public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize,
- CultureInfo? culture)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
+ CultureInfo? culture, sbyte bidiLevel)
{
- return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture);
+ return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture, bidiLevel);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
new file mode 100644
index 00000000000..404956d1e11
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
@@ -0,0 +1,1717 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting.Unicode
+{
+ ///
+ /// Implementation of Unicode bidirectional algorithm (UAX #9)
+ /// https://unicode.org/reports/tr9/
+ ///
+ ///
+ ///
+ /// The Bidi algorithm uses a number of memory arrays for resolved
+ /// types, level information, bracket types, x9 removal maps and
+ /// more...
+ ///
+ ///
+ /// This implementation of the BiDi algorithm has been designed
+ /// to reduce memory pressure on the GC by re-using the same
+ /// work buffers, so instances of this class should be re-used
+ /// as much as possible.
+ ///
+ ///
+ internal sealed class BidiAlgorithm
+ {
+ ///
+ /// The original BiDiClass classes as provided by the caller
+ ///
+ private ArraySlice _originalClasses;
+
+ ///
+ /// Paired bracket types as provided by caller
+ ///
+ private ArraySlice _pairedBracketTypes;
+
+ ///
+ /// Paired bracket values as provided by caller
+ ///
+ private ArraySlice _pairedBracketValues;
+
+ ///
+ /// Try if the incoming data is known to contain brackets
+ ///
+ private bool _hasBrackets;
+
+ ///
+ /// True if the incoming data is known to contain embedding runs
+ ///
+ private bool _hasEmbeddings;
+
+ ///
+ /// True if the incoming data is known to contain isolating runs
+ ///
+ private bool _hasIsolates;
+
+ ///
+ /// Two directional mapping of isolate start/end pairs
+ ///
+ ///
+ /// The forward mapping maps the start index to the end index.
+ /// The reverse mapping maps the end index to the start index.
+ ///
+ private readonly Dictionary _isolatePairs = new Dictionary();
+
+ ///
+ /// The working BiDi classes
+ ///
+ private ArraySlice _workingClasses;
+
+ ///
+ /// The working classes buffer
+ ///
+ private ArrayBuilder _workingClassesBuffer;
+
+ ///
+ /// A slice of the resolved levels
+ ///
+ private ArraySlice _resolvedLevels;
+
+ ///
+ /// The buffer underlying resolvedLevels
+ ///
+ private ArrayBuilder _resolvedLevelsBuffer;
+
+ ///
+ /// The resolve paragraph embedding level
+ ///
+ private sbyte _paragraphEmbeddingLevel;
+
+ ///
+ /// The status stack used during resolution of explicit
+ /// embedding and isolating runs
+ ///
+ private readonly Stack _statusStack = new Stack();
+
+ ///
+ /// Mapping used to virtually remove characters for rule X9
+ ///
+ private ArrayBuilder _x9Map;
+
+ ///
+ /// Re-usable list of level runs
+ ///
+ private readonly List _levelRuns = new List();
+
+ ///
+ /// Mapping for the current isolating sequence, built
+ /// by joining level runs from the x9 map.
+ ///
+ private ArrayBuilder _isolatedRunMapping;
+
+ ///
+ /// A stack of pending isolate openings used by FindIsolatePairs()
+ ///
+ private readonly Stack _pendingIsolateOpenings = new Stack();
+
+ ///
+ /// The level of the isolating run currently being processed
+ ///
+ private int _runLevel;
+
+ ///
+ /// The direction of the isolating run currently being processed
+ ///
+ private BidiClass _runDirection;
+
+ ///
+ /// The length of the isolating run currently being processed
+ ///
+ private int _runLength;
+
+ ///
+ /// A mapped slice of the resolved types for the isolating run currently
+ /// being processed
+ ///
+ private MappedArraySlice _runResolvedClasses;
+
+ ///
+ /// A mapped slice of the original types for the isolating run currently
+ /// being processed
+ ///
+ private MappedArraySlice _runOriginalClasses;
+
+ ///
+ /// A mapped slice of the run levels for the isolating run currently
+ /// being processed
+ ///
+ private MappedArraySlice _runLevels;
+
+ ///
+ /// A mapped slice of the paired bracket types of the isolating
+ /// run currently being processed
+ ///
+ private MappedArraySlice _runBiDiPairedBracketTypes;
+
+ ///
+ /// A mapped slice of the paired bracket values of the isolating
+ /// run currently being processed
+ ///
+ private MappedArraySlice _runPairedBracketValues;
+
+ ///
+ /// Maximum pairing depth for paired brackets
+ ///
+ private const int MaxPairedBracketDepth = 63;
+
+ ///
+ /// Reusable list of pending opening brackets used by the
+ /// LocatePairedBrackets method
+ ///
+ private readonly List _pendingOpeningBrackets = new List();
+
+ ///
+ /// Resolved list of paired brackets
+ ///
+ private readonly List _pairedBrackets = new List();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal BidiAlgorithm()
+ {
+ }
+
+ ///
+ /// Gets a per-thread instance that can be re-used as often
+ /// as necessary.
+ ///
+ public static ThreadLocal Instance { get; } = new ThreadLocal(() => new BidiAlgorithm());
+
+ ///
+ /// Gets the resolved levels.
+ ///
+ public ArraySlice ResolvedLevels => _resolvedLevels;
+
+ ///
+ /// Gets the resolved paragraph embedding level
+ ///
+ public int ResolvedParagraphEmbeddingLevel => _paragraphEmbeddingLevel;
+
+ ///
+ /// Process data from a BiDiData instance
+ ///
+ /// The BiDi Unicode data.
+ public void Process(BidiData data)
+ => Process(
+ data.Classes,
+ data.PairedBracketTypes,
+ data.PairedBracketValues,
+ data.ParagraphEmbeddingLevel,
+ data.HasBrackets,
+ data.HasEmbeddings,
+ data.HasIsolates,
+ null);
+
+ ///
+ /// Processes Bidi Data
+ ///
+ public void Process(
+ ArraySlice types,
+ ArraySlice pairedBracketTypes,
+ ArraySlice pairedBracketValues,
+ sbyte paragraphEmbeddingLevel,
+ bool? hasBrackets,
+ bool? hasEmbeddings,
+ bool? hasIsolates,
+ ArraySlice? outLevels)
+ {
+ // Reset state
+ _isolatePairs.Clear();
+ _workingClassesBuffer.Clear();
+ _levelRuns.Clear();
+ _resolvedLevelsBuffer.Clear();
+
+ // Setup original types and working types
+ _originalClasses = types;
+ _workingClasses = _workingClassesBuffer.Add(types);
+
+ // Capture paired bracket values and types
+ _pairedBracketTypes = pairedBracketTypes;
+ _pairedBracketValues = pairedBracketValues;
+
+ // Store things we know
+ _hasBrackets = hasBrackets ?? _pairedBracketTypes.Length == _originalClasses.Length;
+ _hasEmbeddings = hasEmbeddings ?? true;
+ _hasIsolates = hasIsolates ?? true;
+
+ // Find all isolate pairs
+ FindIsolatePairs();
+
+ // Resolve the paragraph embedding level
+ if (paragraphEmbeddingLevel == 2)
+ {
+ _paragraphEmbeddingLevel = ResolveEmbeddingLevel(_originalClasses);
+ }
+ else
+ {
+ _paragraphEmbeddingLevel = paragraphEmbeddingLevel;
+ }
+
+ // Create resolved levels buffer
+ if (outLevels.HasValue)
+ {
+ if (outLevels.Value.Length != _originalClasses.Length)
+ {
+ throw new ArgumentException("Out levels must be the same length as the input data");
+ }
+
+ _resolvedLevels = outLevels.Value;
+ }
+ else
+ {
+ _resolvedLevels = _resolvedLevelsBuffer.Add(_originalClasses.Length);
+ _resolvedLevels.Fill(_paragraphEmbeddingLevel);
+ }
+
+ // Resolve explicit embedding levels (Rules X1-X8)
+ ResolveExplicitEmbeddingLevels();
+
+ // Build the rule X9 map
+ BuildX9RemovalMap();
+
+ // Process all isolated run sequences
+ ProcessIsolatedRunSequences();
+
+ // Reset whitespace levels
+ ResetWhitespaceLevels();
+
+ // Clean up
+ AssignLevelsToCodePointsRemovedByX9();
+ }
+
+ ///
+ /// Resolve the paragraph embedding level if not explicitly passed
+ /// by the caller. Also used by rule X5c for FSI isolating sequences.
+ ///
+ /// The data to be evaluated
+ /// The resolved embedding level
+ public sbyte ResolveEmbeddingLevel(ReadOnlySlice data)
+ {
+ // P2
+ for (var i = 0; i < data.Length; ++i)
+ {
+ switch (data[i])
+ {
+ case BidiClass.LeftToRight:
+ // P3
+ return 0;
+
+ case BidiClass.ArabicLetter:
+ case BidiClass.RightToLeft:
+ // P3
+ return 1;
+
+ case BidiClass.FirstStrongIsolate:
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.RightToLeftIsolate:
+ // 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))
+ {
+ i -= data.Start;
+ }
+ else
+ {
+ i = data.Length;
+ }
+
+ break;
+ }
+ }
+
+ // P3
+ return 0;
+ }
+
+ ///
+ /// Build a list of matching isolates for a directionality slice
+ /// Implements BD9
+ ///
+ private void FindIsolatePairs()
+ {
+ // Redundant?
+ if (!_hasIsolates)
+ {
+ return;
+ }
+
+ // Lets double check this as we go and clear the flag
+ // if there actually aren't any isolate pairs as this might
+ // mean we can skip some later steps
+ _hasIsolates = false;
+
+ // BD9...
+ _pendingIsolateOpenings.Clear();
+
+ for (var i = 0; i < _originalClasses.Length; i++)
+ {
+ var t = _originalClasses[i];
+
+ switch (t)
+ {
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.RightToLeftIsolate:
+ case BidiClass.FirstStrongIsolate:
+ {
+ _pendingIsolateOpenings.Push(i);
+ _hasIsolates = true;
+ break;
+ }
+ case BidiClass.PopDirectionalIsolate:
+ {
+ if (_pendingIsolateOpenings.Count > 0)
+ {
+ _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i);
+ }
+
+ _hasIsolates = true;
+
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Resolve the explicit embedding levels from the original
+ /// data. Implements rules X1 to X8.
+ ///
+ private void ResolveExplicitEmbeddingLevels()
+ {
+ // Redundant?
+ if (!_hasIsolates && !_hasEmbeddings)
+ {
+ return;
+ }
+
+ // Work variables
+ _statusStack.Clear();
+ var overflowIsolateCount = 0;
+ var overflowEmbeddingCount = 0;
+ var validIsolateCount = 0;
+
+ // Constants
+ const int maxStackDepth = 125;
+
+ // Rule X1 - setup initial state
+ _statusStack.Clear();
+
+ // Neutral
+ _statusStack.Push(new Status(_paragraphEmbeddingLevel, BidiClass.OtherNeutral, false));
+
+ // Process all characters
+ for (var i = 0; i < _originalClasses.Length; i++)
+ {
+ switch (_originalClasses[i])
+ {
+ case BidiClass.RightToLeftEmbedding:
+ {
+ // Rule X2
+ var newLevel = (sbyte)((_statusStack.Peek().EmbeddingLevel + 1) | 1);
+ if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0)
+ {
+ _statusStack.Push(new Status(newLevel, BidiClass.OtherNeutral, false));
+ _resolvedLevels[i] = newLevel;
+ }
+ else if (overflowIsolateCount == 0)
+ {
+ overflowEmbeddingCount++;
+ }
+
+ break;
+ }
+
+ case BidiClass.LeftToRightEmbedding:
+ {
+ // Rule X3
+ var newLevel = (sbyte)((_statusStack.Peek().EmbeddingLevel + 2) & ~1);
+ if (newLevel < maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0)
+ {
+ _statusStack.Push(new Status(newLevel, BidiClass.OtherNeutral, false));
+ _resolvedLevels[i] = newLevel;
+ }
+ else if (overflowIsolateCount == 0)
+ {
+ overflowEmbeddingCount++;
+ }
+
+ break;
+ }
+
+ case BidiClass.RightToLeftOverride:
+ {
+ // Rule X4
+ var newLevel = (sbyte)((_statusStack.Peek().EmbeddingLevel + 1) | 1);
+ if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0)
+ {
+ _statusStack.Push(new Status(newLevel, BidiClass.RightToLeft, false));
+ _resolvedLevels[i] = newLevel;
+ }
+ else if (overflowIsolateCount == 0)
+ {
+ overflowEmbeddingCount++;
+ }
+
+ break;
+ }
+
+ case BidiClass.LeftToRightOverride:
+ {
+ // Rule X5
+ var newLevel = (sbyte)((_statusStack.Peek().EmbeddingLevel + 2) & ~1);
+ if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0)
+ {
+ _statusStack.Push(new Status(newLevel, BidiClass.LeftToRight, false));
+ _resolvedLevels[i] = newLevel;
+ }
+ else if (overflowIsolateCount == 0)
+ {
+ overflowEmbeddingCount++;
+ }
+
+ break;
+ }
+
+ case BidiClass.RightToLeftIsolate:
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.FirstStrongIsolate:
+ {
+ // Rule X5a, X5b and X5c
+ var resolvedIsolate = _originalClasses[i];
+
+ if (resolvedIsolate == BidiClass.FirstStrongIsolate)
+ {
+ if (!_isolatePairs.TryGetValue(i, out var endOfIsolate))
+ {
+ endOfIsolate = _originalClasses.Length;
+ }
+
+ // Rule X5c
+ if (ResolveEmbeddingLevel(_originalClasses.Slice(i + 1,endOfIsolate - (i + 1))) == 1)
+ {
+ resolvedIsolate = BidiClass.RightToLeftIsolate;
+ }
+ else
+ {
+ resolvedIsolate = BidiClass.LeftToRightIsolate;
+ }
+ }
+
+ // Replace RLI's level with current embedding level
+ var tos = _statusStack.Peek();
+ _resolvedLevels[i] = tos.EmbeddingLevel;
+
+ // Apply override
+ if (tos.OverrideStatus != BidiClass.OtherNeutral)
+ {
+ _workingClasses[i] = tos.OverrideStatus;
+ }
+
+ // Work out new level
+ sbyte newLevel;
+ if (resolvedIsolate == BidiClass.RightToLeftIsolate)
+ {
+ newLevel = (sbyte)((tos.EmbeddingLevel + 1) | 1);
+ }
+ else
+ {
+ newLevel = (sbyte)((tos.EmbeddingLevel + 2) & ~1);
+ }
+
+ // Valid?
+ if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0)
+ {
+ validIsolateCount++;
+ _statusStack.Push(new Status(newLevel, BidiClass.OtherNeutral, true));
+ }
+ else
+ {
+ overflowIsolateCount++;
+ }
+
+ break;
+ }
+
+ case BidiClass.BoundaryNeutral:
+ {
+ // Mentioned in rule X6 - "for all types besides ..., BN, ..."
+ // no-op
+ break;
+ }
+
+ default:
+ {
+ // Rule X6
+ var tos = _statusStack.Peek();
+ _resolvedLevels[i] = tos.EmbeddingLevel;
+ if (tos.OverrideStatus != BidiClass.OtherNeutral)
+ {
+ _workingClasses[i] = tos.OverrideStatus;
+ }
+
+ break;
+ }
+
+ case BidiClass.PopDirectionalIsolate:
+ {
+ // Rule X6a
+ if (overflowIsolateCount > 0)
+ {
+ overflowIsolateCount--;
+ }
+ else if (validIsolateCount != 0)
+ {
+ overflowEmbeddingCount = 0;
+ while (!_statusStack.Peek().IsolateStatus)
+ {
+ _statusStack.Pop();
+ }
+
+ _statusStack.Pop();
+ validIsolateCount--;
+ }
+
+ var tos = _statusStack.Peek();
+ _resolvedLevels[i] = tos.EmbeddingLevel;
+ if (tos.OverrideStatus != BidiClass.OtherNeutral)
+ {
+ _workingClasses[i] = tos.OverrideStatus;
+ }
+
+ break;
+ }
+
+ case BidiClass.PopDirectionalFormat:
+ {
+ // Rule X7
+ if (overflowIsolateCount == 0)
+ {
+ if (overflowEmbeddingCount > 0)
+ {
+ overflowEmbeddingCount--;
+ }
+ else if (!_statusStack.Peek().IsolateStatus && _statusStack.Count >= 2)
+ {
+ _statusStack.Pop();
+ }
+ }
+
+ break;
+ }
+
+ case BidiClass.ParagraphSeparator:
+ {
+ // Rule X8
+ _resolvedLevels[i] = _paragraphEmbeddingLevel;
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Build a map to the original data positions that excludes all
+ /// the types defined by rule X9
+ ///
+ private void BuildX9RemovalMap()
+ {
+ // Reserve room for the x9 map
+ _x9Map.Length = _originalClasses.Length;
+
+ if (_hasEmbeddings || _hasIsolates)
+ {
+ // Build a map the removes all x9 characters
+ var j = 0;
+ for (var i = 0; i < _originalClasses.Length; i++)
+ {
+ if (!IsRemovedByX9(_originalClasses[i]))
+ {
+ _x9Map[j++] = i;
+ }
+ }
+
+ // Set the final length
+ _x9Map.Length = j;
+ }
+ else
+ {
+ for (int i = 0, count = _originalClasses.Length; i < count; i++)
+ {
+ _x9Map[i] = i;
+ }
+ }
+ }
+
+ ///
+ /// Find the original character index for an entry in the X9 map
+ ///
+ /// Index in the x9 removal map
+ /// Index to the original data
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int MapX9(int index) => _x9Map[index];
+
+ ///
+ /// Add a new level run
+ ///
+ ///
+ /// This method resolves the sos and eos values for the run
+ /// and adds the run to the list
+ /// ///
+ /// The index of the start of the run (in x9 removed units)
+ /// The length of the run (in x9 removed units)
+ /// The level of the run
+ private void AddLevelRun(int start, int length, int level)
+ {
+ // Get original indices to first and last character in this run
+ var firstCharIndex = MapX9(start);
+ var lastCharIndex = MapX9(start + length - 1);
+
+ // Work out sos
+ var i = firstCharIndex - 1;
+
+ while (i >= 0 && IsRemovedByX9(_originalClasses[i]))
+ {
+ i--;
+ }
+
+ var prevLevel = i < 0 ? _paragraphEmbeddingLevel : _resolvedLevels[i];
+ var sos = DirectionFromLevel(Math.Max(prevLevel, level));
+
+ // Work out eos
+ var lastType = _workingClasses[lastCharIndex];
+ int nextLevel;
+
+ switch (lastType)
+ {
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.RightToLeftIsolate:
+ case BidiClass.FirstStrongIsolate:
+ {
+ nextLevel = _paragraphEmbeddingLevel;
+
+ break;
+ }
+ default:
+ {
+ i = lastCharIndex + 1;
+ while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
+ {
+ i++;
+ }
+
+ nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
+
+ break;
+ }
+ }
+
+ var eos = DirectionFromLevel(Math.Max(nextLevel, level));
+
+ // Add the run
+ _levelRuns.Add(new LevelRun(start, length, level, sos, eos));
+ }
+
+ ///
+ /// Find all runs of the same level, populating the _levelRuns
+ /// collection
+ ///
+ private void FindLevelRuns()
+ {
+ var currentLevel = -1;
+ var runStart = 0;
+
+ for (var i = 0; i < _x9Map.Length; ++i)
+ {
+ int level = _resolvedLevels[MapX9(i)];
+
+ if (level == currentLevel)
+ {
+ continue;
+ }
+
+ if (currentLevel != -1)
+ {
+ AddLevelRun(runStart, i - runStart, currentLevel);
+ }
+
+ currentLevel = level;
+ runStart = i;
+ }
+
+ // Don't forget the final level run
+ if (currentLevel != -1)
+ {
+ AddLevelRun(runStart, _x9Map.Length - runStart, currentLevel);
+ }
+ }
+
+ ///
+ /// Given a character index, find the level run that starts at that position
+ ///
+ /// The index into the original (unmapped) data
+ /// The index of the run that starts at that index
+ private int FindRunForIndex(int index)
+ {
+ for (var i = 0; i < _levelRuns.Count; i++)
+ {
+ // Passed index is for the original non-x9 filtered data, however
+ // the level run ranges are for the x9 filtered data. Convert before
+ // comparing
+ if (MapX9(_levelRuns[i].Start) == index)
+ {
+ return i;
+ }
+ }
+
+ throw new InvalidOperationException("Internal error");
+ }
+
+ ///
+ /// Determine and the process all isolated run sequences
+ ///
+ private void ProcessIsolatedRunSequences()
+ {
+ // Find all runs with the same level
+ FindLevelRuns();
+
+ // Process them one at a time by first building
+ // a mapping using slices from the x9 map for each
+ // run section that needs to be joined together to
+ // form an complete run. That full run mapping
+ // will be placed in _isolatedRunMapping and then
+ // processed by ProcessIsolatedRunSequence().
+ while (_levelRuns.Count > 0)
+ {
+ // Clear the mapping
+ _isolatedRunMapping.Clear();
+
+ // Combine mappings from this run and all runs that continue on from it
+ var runIndex = 0;
+ BidiClass eos;
+ var sos = _levelRuns[0].Sos;
+ var level = _levelRuns[0].Level;
+
+ while (true)
+ {
+ // Get the run
+ var r = _levelRuns[runIndex];
+
+ // The eos of the isolating run is the eos of the
+ // last level run that comprises it.
+ eos = r.Eos;
+
+ // Remove this run as we've now processed it
+ _levelRuns.RemoveAt(runIndex);
+
+ // Add the x9 map indices for the run range to the mapping
+ // for this isolated run
+ _isolatedRunMapping.Add(_x9Map.AsSlice(r.Start, r.Length));
+
+ // Get the last character and see if it's an isolating run with a matching
+ // PDI and concatenate that run to this one
+ 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))
+ {
+ // Find the continuing run index
+ runIndex = FindRunForIndex(nextRunIndex);
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // Process this isolated run
+ ProcessIsolatedRunSequence(sos, eos, level);
+ }
+ }
+
+ ///
+ /// Process a single isolated run sequence, where the character sequence
+ /// mapping is currently held in _isolatedRunMapping.
+ ///
+ 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());
+ if (_hasBrackets)
+ {
+ _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, _isolatedRunMapping.AsSlice());
+ _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, _isolatedRunMapping.AsSlice());
+ }
+
+ _runLevel = runLevel;
+ _runDirection = DirectionFromLevel(runLevel);
+ _runLength = _runResolvedClasses.Length;
+
+ // By tracking the types of characters known to be in the current run, we can
+ // skip some of the rules that we know won't apply. The flags will be
+ // initialized while we're processing rule W1 below.
+ var hasEN = false;
+ var hasAL = false;
+ var hasES = false;
+ var hasCS = false;
+ var hasAN = false;
+ var hasET = false;
+
+ // Rule W1
+ // Also, set hasXX flags
+ int i;
+ var previousClass = sos;
+
+ for (i = 0; i < _runLength; i++)
+ {
+ var resolvedClass = _runResolvedClasses[i];
+
+ switch (resolvedClass)
+ {
+ case BidiClass.NonspacingMark:
+ _runResolvedClasses[i] = previousClass;
+ break;
+
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.RightToLeftIsolate:
+ case BidiClass.FirstStrongIsolate:
+ case BidiClass.PopDirectionalIsolate:
+ previousClass = BidiClass.OtherNeutral;
+ break;
+
+ case BidiClass.EuropeanNumber:
+ hasEN = true;
+ previousClass = resolvedClass;
+ break;
+
+ case BidiClass.ArabicLetter:
+ hasAL = true;
+ previousClass = resolvedClass;
+ break;
+
+ case BidiClass.EuropeanSeparator:
+ hasES = true;
+ previousClass = resolvedClass;
+ break;
+
+ case BidiClass.CommonSeparator:
+ hasCS = true;
+ previousClass = resolvedClass;
+ break;
+
+ case BidiClass.ArabicNumber:
+ hasAN = true;
+ previousClass = resolvedClass;
+ break;
+
+ case BidiClass.EuropeanTerminator:
+ hasET = true;
+ previousClass = resolvedClass;
+ break;
+
+ default:
+ previousClass = resolvedClass;
+ break;
+ }
+ }
+
+ // Rule W2
+ if (hasEN)
+ {
+ for (i = 0; i < _runLength; i++)
+ {
+ if (_runResolvedClasses[i] != BidiClass.EuropeanNumber)
+ {
+ continue;
+ }
+
+ for (var j = i - 1; j >= 0; j--)
+ {
+ var resolvedClass = _runResolvedClasses[j];
+
+ switch (resolvedClass)
+ {
+ case BidiClass.LeftToRight:
+ case BidiClass.RightToLeft:
+ case BidiClass.ArabicLetter:
+ {
+ if (resolvedClass == BidiClass.ArabicLetter)
+ {
+ _runResolvedClasses[i] = BidiClass.ArabicNumber;
+ hasAN = true;
+ }
+
+ j = -1;
+
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Rule W3
+ if (hasAL)
+ {
+ for (i = 0; i < _runLength; i++)
+ {
+ if (_runResolvedClasses[i] == BidiClass.ArabicLetter)
+ {
+ _runResolvedClasses[i] = BidiClass.RightToLeft;
+ }
+ }
+ }
+
+ // Rule W4
+ if ((hasES || hasCS) && (hasEN || hasAN))
+ {
+ for (i = 1; i < _runLength - 1; ++i)
+ {
+ ref var resolvedClass = ref _runResolvedClasses[i];
+
+ if (resolvedClass == BidiClass.EuropeanSeparator)
+ {
+ var previousSeparatorClass = _runResolvedClasses[i - 1];
+ var nextSeparatorClass = _runResolvedClasses[i + 1];
+
+ if (previousSeparatorClass == BidiClass.EuropeanNumber && nextSeparatorClass == BidiClass.EuropeanNumber)
+ {
+ // ES between EN and EN
+ resolvedClass = BidiClass.EuropeanNumber;
+ }
+ }
+ else if (resolvedClass == BidiClass.CommonSeparator)
+ {
+ var previousSeparatorClass = _runResolvedClasses[i - 1];
+ var nextSeparatorClass = _runResolvedClasses[i + 1];
+
+ if ((previousSeparatorClass == BidiClass.ArabicNumber && nextSeparatorClass == BidiClass.ArabicNumber) ||
+ (previousSeparatorClass == BidiClass.EuropeanNumber && nextSeparatorClass == BidiClass.EuropeanNumber))
+ {
+ // CS between (AN and AN) or (EN and EN)
+ resolvedClass = previousSeparatorClass;
+ }
+ }
+ }
+ }
+
+ // Rule W5
+ if (hasET && hasEN)
+ {
+ for (i = 0; i < _runLength; ++i)
+ {
+ if (_runResolvedClasses[i] != BidiClass.EuropeanTerminator)
+ {
+ continue;
+ }
+
+ // Locate end of sequence
+ var sequenceStart = i;
+ var sequenceEnd = i;
+
+ while (sequenceEnd < _runLength && _runResolvedClasses[sequenceEnd] == BidiClass.EuropeanTerminator)
+ {
+ sequenceEnd++;
+ }
+
+ // Preceded by, or followed by EN?
+ if ((sequenceStart == 0 ? sos : _runResolvedClasses[sequenceStart - 1]) == BidiClass.EuropeanNumber
+ || (sequenceEnd == _runLength ? eos : _runResolvedClasses[sequenceEnd]) == BidiClass.EuropeanNumber)
+ {
+ // Change the entire range
+ for (var j = sequenceStart; i < sequenceEnd; ++i)
+ {
+ _runResolvedClasses[i] = BidiClass.EuropeanNumber;
+ }
+ }
+
+ // continue at end of sequence
+ i = sequenceEnd;
+ }
+ }
+
+ // Rule W6
+ if (hasES || hasET || hasCS)
+ {
+ for (i = 0; i < _runLength; ++i)
+ {
+ ref var resolvedClass = ref _runResolvedClasses[i];
+
+ switch (resolvedClass)
+ {
+ case BidiClass.EuropeanSeparator:
+ case BidiClass.EuropeanTerminator:
+ case BidiClass.CommonSeparator:
+ {
+ resolvedClass = BidiClass.OtherNeutral;
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Rule W7.
+ if (hasEN)
+ {
+ var previousStrongClass = sos;
+
+ for (i = 0; i < _runLength; ++i)
+ {
+ ref var resolvedClass = ref _runResolvedClasses[i];
+
+ switch (resolvedClass)
+ {
+ case BidiClass.EuropeanNumber:
+ {
+ // If prev strong type was an L change this to L too
+ if (previousStrongClass == BidiClass.LeftToRight)
+ {
+ _runResolvedClasses[i] = BidiClass.LeftToRight;
+ }
+
+ break;
+ }
+
+ case BidiClass.LeftToRight:
+ case BidiClass.RightToLeft:
+ {
+ // Remember previous strong type (NB: AL should already be changed to R)
+ previousStrongClass = resolvedClass;
+ break;
+ }
+ }
+ }
+ }
+
+ // Rule N0 - process bracket pairs
+ if (_hasBrackets)
+ {
+ int count;
+ var pairedBrackets = LocatePairedBrackets();
+
+ for (i = 0, count = pairedBrackets.Count; i < count; i++)
+ {
+ var pairedBracket = pairedBrackets[i];
+
+ var strongDirection = InspectPairedBracket(pairedBracket);
+
+ // Case "d" - no strong types in the brackets, ignore
+ if (strongDirection == BidiClass.OtherNeutral)
+ {
+ continue;
+ }
+
+ // Case "b" - strong type found that matches the embedding direction
+ if ((strongDirection == BidiClass.LeftToRight || strongDirection == BidiClass.RightToLeft) && strongDirection == _runDirection)
+ {
+ SetPairedBracketDirection(pairedBracket, strongDirection);
+ continue;
+ }
+
+ // Case "c" - found opposite strong type found, look before to establish context
+ strongDirection = InspectBeforePairedBracket(pairedBracket, sos);
+
+ if (strongDirection == _runDirection || strongDirection == BidiClass.OtherNeutral)
+ {
+ strongDirection = _runDirection;
+ }
+
+ SetPairedBracketDirection(pairedBracket, strongDirection);
+ }
+ }
+
+ // Rules N1 and N2 - resolve neutral types
+ for (i = 0; i < _runLength; ++i)
+ {
+ var resolvedClass = _runResolvedClasses[i];
+
+ if (IsNeutralClass(resolvedClass))
+ {
+ // Locate end of sequence
+ var seqStart = i;
+ var seqEnd = i;
+
+ while (seqEnd < _runLength && IsNeutralClass(_runResolvedClasses[seqEnd]))
+ {
+ seqEnd++;
+ }
+
+ // Work out the preceding class
+ BidiClass classBefore;
+
+ if (seqStart == 0)
+ {
+ classBefore = sos;
+ }
+ else
+ {
+ classBefore = _runResolvedClasses[seqStart - 1];
+
+ switch (classBefore)
+ {
+ case BidiClass.ArabicNumber:
+ case BidiClass.EuropeanNumber:
+ {
+ classBefore = BidiClass.RightToLeft;
+
+ break;
+ }
+ }
+ }
+
+ // Work out the following class
+ BidiClass classAfter;
+
+ if (seqEnd == _runLength)
+ {
+ classAfter = eos;
+ }
+ else
+ {
+ classAfter = _runResolvedClasses[seqEnd];
+
+ switch (classAfter)
+ {
+ case BidiClass.ArabicNumber:
+ case BidiClass.EuropeanNumber:
+ {
+ classAfter = BidiClass.RightToLeft;
+
+ break;
+ }
+ }
+ }
+
+ // Work out the final resolved type
+ BidiClass finalResolveClass;
+
+ if (classBefore == classAfter)
+ {
+ // Rule N1
+ finalResolveClass = classBefore;
+ }
+ else
+ {
+ // Rule N2
+ finalResolveClass = _runDirection;
+ }
+
+ // Apply changes
+ for (var j = seqStart; j < seqEnd; j++)
+ {
+ _runResolvedClasses[j] = finalResolveClass;
+ }
+
+ // continue after this run
+ i = seqEnd;
+ }
+ }
+
+ // Rules I1 and I2 - resolve implicit types
+ if ((_runLevel & 0x01) == 0)
+ {
+ // Rule I1 - even
+ for (i = 0; i < _runLength; i++)
+ {
+ var resolvedClass = _runResolvedClasses[i];
+ ref var currentRunLevel = ref _runLevels[i];
+
+ switch (resolvedClass)
+ {
+ case BidiClass.RightToLeft:
+ {
+ currentRunLevel++;
+ break;
+ }
+ case BidiClass.ArabicNumber:
+ case BidiClass.EuropeanNumber:
+ {
+ currentRunLevel += 2;
+
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ // Rule I2 - odd
+ for (i = 0; i < _runLength; i++)
+ {
+ var resolvedClass = _runResolvedClasses[i];
+ ref var currentRunLevel = ref _runLevels[i];
+
+ if (resolvedClass != BidiClass.RightToLeft)
+ {
+ currentRunLevel++;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Locate all pair brackets in the current isolating run
+ ///
+ /// A sorted list of BracketPairs
+ private List LocatePairedBrackets()
+ {
+ // Clear work collections
+ _pendingOpeningBrackets.Clear();
+ _pairedBrackets.Clear();
+
+ // Since List.Sort is expensive on memory if called often (it internally
+ // allocates an ArraySorted object) and since we will rarely have many
+ // items in this list (most paragraphs will only have a handful of bracket
+ // pairs - if that), we use a simple linear lookup and insert most of the
+ // time. If there are more that `sortLimit` paired brackets we abort th
+ // linear searching/inserting and using List.Sort at the end.
+ const int sortLimit = 8;
+
+ // Process all characters in the run, looking for paired brackets
+ for (int i = 0, length = _runLength; i < length; i++)
+ {
+ // Ignore non-neutral characters
+ if (_runResolvedClasses[i] != BidiClass.OtherNeutral)
+ {
+ continue;
+ }
+
+ switch (_runBiDiPairedBracketTypes[i])
+ {
+ case BidiPairedBracketType.Open:
+ if (_pendingOpeningBrackets.Count == MaxPairedBracketDepth)
+ {
+ goto exit;
+ }
+
+ _pendingOpeningBrackets.Insert(0, i);
+ break;
+
+ case BidiPairedBracketType.Close:
+ // see if there is a match
+ for (var j = 0; j < _pendingOpeningBrackets.Count; j++)
+ {
+ if (_runPairedBracketValues[i] != _runPairedBracketValues[_pendingOpeningBrackets[j]])
+ {
+ continue;
+ }
+
+ // Add this paired bracket set
+ var opener = _pendingOpeningBrackets[j];
+
+ if (_pairedBrackets.Count < sortLimit)
+ {
+ var ppi = 0;
+ while (ppi < _pairedBrackets.Count && _pairedBrackets[ppi].OpeningIndex < opener)
+ {
+ ppi++;
+ }
+
+ _pairedBrackets.Insert(ppi, new BracketPair(opener, i));
+ }
+ else
+ {
+ _pairedBrackets.Add(new BracketPair(opener, i));
+ }
+
+ // remove up to and including matched opener
+ _pendingOpeningBrackets.RemoveRange(0, j + 1);
+ break;
+ }
+
+ break;
+ }
+ }
+
+ exit:
+
+ // Is a sort pending?
+ if (_pairedBrackets.Count > sortLimit)
+ {
+ _pairedBrackets.Sort();
+ }
+
+ return _pairedBrackets;
+ }
+
+ ///
+ /// Inspect a paired bracket set and determine its strong direction
+ ///
+ /// The paired bracket to be inspected
+ /// The direction of the bracket set content
+ private BidiClass InspectPairedBracket(in BracketPair bracketPair)
+ {
+ var directionFromLevel = DirectionFromLevel(_runLevel);
+ var directionOpposite = BidiClass.OtherNeutral;
+
+ for (var i = bracketPair.OpeningIndex + 1; i < bracketPair.ClosingIndex; i++)
+ {
+ var dir = GetStrongClassN0(_runResolvedClasses[i]);
+
+ if (dir == BidiClass.OtherNeutral)
+ {
+ continue;
+ }
+
+ if (dir == directionFromLevel)
+ {
+ return dir;
+ }
+
+ directionOpposite = dir;
+ }
+
+ return directionOpposite;
+ }
+
+ ///
+ /// Look for a strong type before a paired bracket
+ ///
+ /// The paired bracket set to be inspected
+ /// The sos in case nothing found before the bracket
+ /// The strong direction before the brackets
+ private BidiClass InspectBeforePairedBracket(in BracketPair bracketPair, BidiClass sos)
+ {
+ for (var i = bracketPair.OpeningIndex - 1; i >= 0; --i)
+ {
+ var direction = GetStrongClassN0(_runResolvedClasses[i]);
+
+ if (direction != BidiClass.OtherNeutral)
+ {
+ return direction;
+ }
+ }
+
+ return sos;
+ }
+
+ ///
+ /// Sets the direction of a bracket pair, including setting the direction of
+ /// NSM's inside the brackets and following.
+ ///
+ /// The paired brackets
+ /// The resolved direction for the bracket pair
+ private void SetPairedBracketDirection(in BracketPair bracketPair, BidiClass direction)
+ {
+ // Set the direction of the brackets
+ _runResolvedClasses[bracketPair.OpeningIndex] = direction;
+ _runResolvedClasses[bracketPair.ClosingIndex] = direction;
+
+ // Set the directionality of NSM's inside the brackets
+ for (var i = bracketPair.OpeningIndex + 1; i < bracketPair.ClosingIndex; i++)
+ {
+ if (_runOriginalClasses[i] == BidiClass.NonspacingMark)
+ {
+ _runOriginalClasses[i] = direction;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // Set the directionality of NSM's following the brackets
+ for (var i = bracketPair.ClosingIndex + 1; i < _runLength; i++)
+ {
+ if (_runOriginalClasses[i] == BidiClass.NonspacingMark)
+ {
+ _runResolvedClasses[i] = direction;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Resets whitespace levels. Implements rule L1
+ ///
+ private void ResetWhitespaceLevels()
+ {
+ for (var i = 0; i < _resolvedLevels.Length; i++)
+ {
+ var originalClass = _originalClasses[i];
+
+ switch (originalClass)
+ {
+ case BidiClass.ParagraphSeparator:
+ case BidiClass.SegmentSeparator:
+ {
+ // Rule L1, clauses one and two.
+ _resolvedLevels[i] = _paragraphEmbeddingLevel;
+
+ // Rule L1, clause three.
+ for (var j = i - 1; j >= 0; --j)
+ {
+ if (IsWhitespace(_originalClasses[j]))
+ {
+ // including format codes
+ _resolvedLevels[j] = _paragraphEmbeddingLevel;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
+ // Rule L1, clause four.
+ for (var j = _resolvedLevels.Length - 1; j >= 0; j--)
+ {
+ if (IsWhitespace(_originalClasses[j]))
+ { // including format codes
+ _resolvedLevels[j] = _paragraphEmbeddingLevel;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Assign levels to any characters that would be have been
+ /// removed by rule X9. The idea is to keep level runs together
+ /// that would otherwise be broken by an interfering isolate/embedding
+ /// control character.
+ ///
+ private void AssignLevelsToCodePointsRemovedByX9()
+ {
+ // Redundant?
+ if (!_hasIsolates && !_hasEmbeddings)
+ {
+ return;
+ }
+
+ // No-op?
+ if (_workingClasses.Length == 0)
+ {
+ return;
+ }
+
+ // Fix up first character
+ if (_resolvedLevels[0] < 0)
+ {
+ _resolvedLevels[0] = _paragraphEmbeddingLevel;
+ }
+
+ if (IsRemovedByX9(_originalClasses[0]))
+ {
+ _workingClasses[0] = _originalClasses[0];
+ }
+
+ for (int i = 1, length = _workingClasses.Length; i < length; i++)
+ {
+ var originalClass = _originalClasses[i];
+
+ if (IsRemovedByX9(originalClass))
+ {
+ _workingClasses[i] = originalClass;
+ _resolvedLevels[i] = _resolvedLevels[i - 1];
+ }
+ }
+ }
+
+ ///
+ /// Check if a directionality type represents whitespace
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsWhitespace(BidiClass biDiClass)
+ {
+ switch (biDiClass)
+ {
+ case BidiClass.LeftToRightEmbedding:
+ case BidiClass.RightToLeftEmbedding:
+ case BidiClass.LeftToRightOverride:
+ case BidiClass.RightToLeftOverride:
+ case BidiClass.PopDirectionalFormat:
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.RightToLeftIsolate:
+ case BidiClass.FirstStrongIsolate:
+ case BidiClass.PopDirectionalIsolate:
+ case BidiClass.BoundaryNeutral:
+ case BidiClass.WhiteSpace:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Convert a level to a direction where odd is RTL and
+ /// even is LTR
+ ///
+ /// The level to convert
+ /// A directionality
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static BidiClass DirectionFromLevel(int level)
+ => ((level & 0x1) == 0) ? BidiClass.LeftToRight : BidiClass.RightToLeft;
+
+ ///
+ /// Helper to check if a directionality is removed by rule X9
+ ///
+ /// The bidi type to check
+ /// True if rule X9 would remove this character; otherwise false
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsRemovedByX9(BidiClass biDiClass)
+ {
+ switch (biDiClass)
+ {
+ case BidiClass.LeftToRightEmbedding:
+ case BidiClass.RightToLeftEmbedding:
+ case BidiClass.LeftToRightOverride:
+ case BidiClass.RightToLeftOverride:
+ case BidiClass.PopDirectionalFormat:
+ case BidiClass.BoundaryNeutral:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Check if a a directionality is neutral for rules N1 and N2
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsNeutralClass(BidiClass direction)
+ {
+ switch (direction)
+ {
+ case BidiClass.ParagraphSeparator:
+ case BidiClass.SegmentSeparator:
+ case BidiClass.WhiteSpace:
+ case BidiClass.OtherNeutral:
+ case BidiClass.RightToLeftIsolate:
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.FirstStrongIsolate:
+ case BidiClass.PopDirectionalIsolate:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Maps a direction to a strong class for rule N0
+ ///
+ /// The direction to map
+ /// A strong direction - R, L or ON
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static BidiClass GetStrongClassN0(BidiClass direction)
+ {
+ switch (direction)
+ {
+ case BidiClass.EuropeanNumber:
+ case BidiClass.ArabicNumber:
+ case BidiClass.ArabicLetter:
+ case BidiClass.RightToLeft:
+ return BidiClass.RightToLeft;
+ case BidiClass.LeftToRight:
+ return BidiClass.LeftToRight;
+ default:
+ return BidiClass.OtherNeutral;
+ }
+ }
+
+ ///
+ /// Hold the start and end index of a pair of brackets
+ ///
+ private readonly struct BracketPair : IComparable
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Index of the opening bracket
+ /// Index of the closing bracket
+ public BracketPair(int openingIndex, int closingIndex)
+ {
+ OpeningIndex = openingIndex;
+ ClosingIndex = closingIndex;
+ }
+
+ ///
+ /// Gets the index of the opening bracket
+ ///
+ public int OpeningIndex { get; }
+
+ ///
+ /// Gets the index of the closing bracket
+ ///
+ public int ClosingIndex { get; }
+
+ public int CompareTo(BracketPair other)
+ => OpeningIndex.CompareTo(other.OpeningIndex);
+ }
+
+ ///
+ /// Status stack entry used while resolving explicit
+ /// embedding levels
+ ///
+ private readonly struct Status
+ {
+ public Status(sbyte embeddingLevel, BidiClass overrideStatus, bool isolateStatus)
+ {
+ EmbeddingLevel = embeddingLevel;
+ OverrideStatus = overrideStatus;
+ IsolateStatus = isolateStatus;
+ }
+
+ public sbyte EmbeddingLevel { get; }
+
+ public BidiClass OverrideStatus { get; }
+
+ public bool IsolateStatus { get; }
+ }
+
+ ///
+ /// Provides information about a level run - a continuous
+ /// sequence of equal levels.
+ ///
+ private readonly struct LevelRun
+ {
+ public LevelRun(int start, int length, int level, BidiClass sos, BidiClass eos)
+ {
+ Start = start;
+ Length = length;
+ Level = level;
+ Sos = sos;
+ Eos = eos;
+ }
+
+ public int Start { get; }
+
+ public int Length { get; }
+
+ public int Level { get; }
+
+ public BidiClass Sos { get; }
+
+ public BidiClass Eos { get; }
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs
index ad3cc9141b3..28c6fee089b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs
@@ -1,6 +1,6 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
- public enum BiDiClass
+ public enum BidiClass
{
LeftToRight, //L
ArabicLetter, //AL
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiData.cs
new file mode 100644
index 00000000000..9d76d56376c
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiData.cs
@@ -0,0 +1,182 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/
+
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting.Unicode
+{
+ ///
+ /// Represents a unicode string and all associated attributes
+ /// for each character required for the bidirectional Unicode algorithm
+ ///
+ internal class BidiData
+ {
+ private ArrayBuilder _classes;
+ private ArrayBuilder _pairedBracketTypes;
+ private ArrayBuilder _pairedBracketValues;
+ private ArrayBuilder _savedClasses;
+ private ArrayBuilder _savedPairedBracketTypes;
+ private ArrayBuilder _tempLevelBuffer;
+
+ public BidiData(sbyte paragraphEmbeddingLevel = 0)
+ {
+ ParagraphEmbeddingLevel = paragraphEmbeddingLevel;
+ }
+
+ public BidiData(ReadOnlySlice text, sbyte paragraphEmbeddingLevel = 0) : this(paragraphEmbeddingLevel)
+ {
+ Append(text);
+ }
+
+ public sbyte ParagraphEmbeddingLevel { get; private set; }
+
+ public bool HasBrackets { get; private set; }
+
+ public bool HasEmbeddings { get; private set; }
+
+ public bool HasIsolates { get; private set; }
+
+ ///
+ /// Gets the length of the data held by the BidiData
+ ///
+ public int Length{get; private set; }
+
+ ///
+ /// Gets the bidi character type of each code point
+ ///
+ public ArraySlice Classes { get; private set; }
+
+ ///
+ /// Gets the paired bracket type for each code point
+ ///
+ public ArraySlice PairedBracketTypes { get; private set; }
+
+ ///
+ /// Gets the paired bracket value for code point
+ ///
+ ///
+ /// The paired bracket values are the code points
+ /// of each character where the opening code point
+ /// is replaced with the closing code point for easier
+ /// matching. Also, bracket code points are mapped
+ /// to their canonical equivalents
+ ///
+ public ArraySlice PairedBracketValues { get; private set; }
+
+ public void Append(ReadOnlySlice text)
+ {
+ _classes.Add(text.Length);
+ _pairedBracketTypes.Add(text.Length);
+ _pairedBracketValues.Add(text.Length);
+
+ var i = Length;
+
+ var codePointEnumerator = new CodepointEnumerator(text);
+
+ while (codePointEnumerator.MoveNext())
+ {
+ var codepoint = codePointEnumerator.Current;
+
+ // Look up BiDiClass
+ var dir = codepoint.BiDiClass;
+
+ _classes[i] = dir;
+
+ switch (dir)
+ {
+ case BidiClass.LeftToRightEmbedding:
+ case BidiClass.LeftToRightOverride:
+ case BidiClass.RightToLeftEmbedding:
+ case BidiClass.RightToLeftOverride:
+ case BidiClass.PopDirectionalFormat:
+ {
+ HasEmbeddings = true;
+ break;
+ }
+
+ case BidiClass.LeftToRightIsolate:
+ case BidiClass.RightToLeftIsolate:
+ case BidiClass.FirstStrongIsolate:
+ case BidiClass.PopDirectionalIsolate:
+ {
+ HasIsolates = true;
+ break;
+ }
+ }
+
+ // Lookup paired bracket types
+ var pbt = codepoint.PairedBracketType;
+
+ _pairedBracketTypes[i] = pbt;
+
+ if (pbt == BidiPairedBracketType.Open)
+ {
+ // Opening bracket types can never have a null pairing.
+ codepoint.TryGetPairedBracket(out var paired);
+
+ _pairedBracketValues[i] = Codepoint.GetCanonicalType(paired).Value;
+
+ HasBrackets = true;
+ }
+ else if (pbt == BidiPairedBracketType.Close)
+ {
+ _pairedBracketValues[i] = Codepoint.GetCanonicalType(codepoint).Value;
+
+ HasBrackets = true;
+ }
+
+ i++;
+ }
+
+ Length = i;
+
+ Classes = _classes.AsSlice(0, Length);
+ PairedBracketTypes = _pairedBracketTypes.AsSlice(0, Length);
+ PairedBracketValues = _pairedBracketValues.AsSlice(0, Length);
+ }
+
+ ///
+ /// Save the Types and PairedBracketTypes of this BiDiData
+ ///
+ ///
+ /// This is used when processing embedded style runs with
+ /// BiDiClass overrides. Text layout process saves the data,
+ /// overrides the style runs to neutral, processes the bidi
+ /// data for the entire paragraph and then restores this data
+ /// before processing the embedded runs.
+ ///
+ public void SaveTypes()
+ {
+ // Capture the types data
+ _savedClasses.Clear();
+ _savedClasses.Add(_classes.AsSlice());
+ _savedPairedBracketTypes.Clear();
+ _savedPairedBracketTypes.Add(_pairedBracketTypes.AsSlice());
+ }
+
+ ///
+ /// Restore the data saved by SaveTypes
+ ///
+ public void RestoreTypes()
+ {
+ _classes.Clear();
+ _classes.Add(_savedClasses.AsSlice());
+ _pairedBracketTypes.Clear();
+ _pairedBracketTypes.Add(_savedPairedBracketTypes.AsSlice());
+ }
+
+ ///
+ /// Gets a temporary level buffer. Used by the text layout process when
+ /// resolving style runs with different BiDiClass.
+ ///
+ /// Length of the required ExpandableBuffer
+ /// An uninitialized level ExpandableBuffer
+ public ArraySlice GetTempLevelBuffer(int length)
+ {
+ _tempLevelBuffer.Clear();
+
+ return _tempLevelBuffer.Add(length, false);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs
new file mode 100644
index 00000000000..f204497eb1f
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs
@@ -0,0 +1,9 @@
+namespace Avalonia.Media.TextFormatting.Unicode
+{
+ public enum BidiPairedBracketType
+ {
+ None, //n
+ Close, //c
+ Open, //o
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
index 43a95310c6d..39440f6fcf4 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
@@ -1,4 +1,5 @@
-using Avalonia.Utilities;
+using System.Runtime.CompilerServices;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
@@ -30,10 +31,15 @@ public Codepoint(int value)
public Script Script => UnicodeData.GetScript(Value);
///
- /// Gets the .
+ /// Gets the .
///
- public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(Value);
+ public BidiClass BiDiClass => UnicodeData.GetBiDiClass(Value);
+ ///
+ /// Gets the .
+ ///
+ public BidiPairedBracketType PairedBracketType => UnicodeData.GetBiDiPairedBracketType(Value);
+
///
/// Gets the .
///
@@ -93,6 +99,52 @@ public bool IsWhiteSpace
return false;
}
}
+
+ ///
+ /// Gets the canonical representation of a given codepoint.
+ ///
+ ///
+ /// The code point to be mapped.
+ /// The mapped canonical code point, or the passed .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Codepoint GetCanonicalType(Codepoint codePoint)
+ {
+ if (codePoint.Value == 0x3008)
+ {
+ return new Codepoint(0x2329);
+ }
+
+ if (codePoint.Value == 0x3009)
+ {
+ return new Codepoint(0x232A);
+ }
+
+ return codePoint;
+ }
+
+ ///
+ /// Gets the codepoint representing the bracket pairing for this instance.
+ ///
+ ///
+ /// When this method returns, contains the codepoint representing the bracket pairing for this instance;
+ /// otherwise, the default value for the type of the parameter.
+ /// This parameter is passed uninitialized.
+ /// .
+ /// if this instance has a bracket pairing; otherwise,
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetPairedBracket(out Codepoint codepoint)
+ {
+ if (PairedBracketType == BidiPairedBracketType.None)
+ {
+ codepoint = default;
+
+ return false;
+ }
+
+ codepoint = UnicodeData.GetBiDiPairedBracket(Value);
+
+ return true;
+ }
public static implicit operator int(Codepoint codepoint)
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs
index 388a7d257d2..af5d9ff752d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs
@@ -174,5 +174,336 @@ public static string GetTag(Script script)
}
return s_scriptToTag[script];
}
+
+ private static readonly Dictionary s_tagToScript =
+ new Dictionary{
+ { "Zzzz", Script.Unknown},
+ { "Zyyy", Script.Common},
+ { "Zinh", Script.Inherited},
+ { "Adlm", Script.Adlam},
+ { "Aghb", Script.CaucasianAlbanian},
+ { "Ahom", Script.Ahom},
+ { "Arab", Script.Arabic},
+ { "Armi", Script.ImperialAramaic},
+ { "Armn", Script.Armenian},
+ { "Avst", Script.Avestan},
+ { "Bali", Script.Balinese},
+ { "Bamu", Script.Bamum},
+ { "Bass", Script.BassaVah},
+ { "Batk", Script.Batak},
+ { "Beng", Script.Bengali},
+ { "Bhks", Script.Bhaiksuki},
+ { "Bopo", Script.Bopomofo},
+ { "Brah", Script.Brahmi},
+ { "Brai", Script.Braille},
+ { "Bugi", Script.Buginese},
+ { "Buhd", Script.Buhid},
+ { "Cakm", Script.Chakma},
+ { "Cans", Script.CanadianAboriginal},
+ { "Cari", Script.Carian},
+ { "Cham", Script.Cham},
+ { "Cher", Script.Cherokee},
+ { "Chrs", Script.Chorasmian},
+ { "Copt", Script.Coptic},
+ { "Cprt", Script.Cypriot},
+ { "Cyrl", Script.Cyrillic},
+ { "Deva", Script.Devanagari},
+ { "Diak", Script.DivesAkuru},
+ { "Dogr", Script.Dogra},
+ { "Dsrt", Script.Deseret},
+ { "Dupl", Script.Duployan},
+ { "Egyp", Script.EgyptianHieroglyphs},
+ { "Elba", Script.Elbasan},
+ { "Elym", Script.Elymaic},
+ { "Ethi", Script.Ethiopic},
+ { "Geor", Script.Georgian},
+ { "Glag", Script.Glagolitic},
+ { "Gong", Script.GunjalaGondi},
+ { "Gonm", Script.MasaramGondi},
+ { "Goth", Script.Gothic},
+ { "Gran", Script.Grantha},
+ { "Grek", Script.Greek},
+ { "Gujr", Script.Gujarati},
+ { "Guru", Script.Gurmukhi},
+ { "Hang", Script.Hangul},
+ { "Hani", Script.Han},
+ { "Hano", Script.Hanunoo},
+ { "Hatr", Script.Hatran},
+ { "Hebr", Script.Hebrew},
+ { "Hira", Script.Hiragana},
+ { "Hluw", Script.AnatolianHieroglyphs},
+ { "Hmng", Script.PahawhHmong},
+ { "Hmnp", Script.NyiakengPuachueHmong},
+ { "Hrkt", Script.KatakanaOrHiragana},
+ { "Hung", Script.OldHungarian},
+ { "Ital", Script.OldItalic},
+ { "Java", Script.Javanese},
+ { "Kali", Script.KayahLi},
+ { "Kana", Script.Katakana},
+ { "Khar", Script.Kharoshthi},
+ { "Khmr", Script.Khmer},
+ { "Khoj", Script.Khojki},
+ { "Kits", Script.KhitanSmallScript},
+ { "Knda", Script.Kannada},
+ { "Kthi", Script.Kaithi},
+ { "Lana", Script.TaiTham},
+ { "Laoo", Script.Lao},
+ { "Latn", Script.Latin},
+ { "Lepc", Script.Lepcha},
+ { "Limb", Script.Limbu},
+ { "Lina", Script.LinearA},
+ { "Linb", Script.LinearB},
+ { "Lisu", Script.Lisu},
+ { "Lyci", Script.Lycian},
+ { "Lydi", Script.Lydian},
+ { "Mahj", Script.Mahajani},
+ { "Maka", Script.Makasar},
+ { "Mand", Script.Mandaic},
+ { "Mani", Script.Manichaean},
+ { "Marc", Script.Marchen},
+ { "Medf", Script.Medefaidrin},
+ { "Mend", Script.MendeKikakui},
+ { "Merc", Script.MeroiticCursive},
+ { "Mero", Script.MeroiticHieroglyphs},
+ { "Mlym", Script.Malayalam},
+ { "Modi", Script.Modi},
+ { "Mong", Script.Mongolian},
+ { "Mroo", Script.Mro},
+ { "Mtei", Script.MeeteiMayek},
+ { "Mult", Script.Multani},
+ { "Mymr", Script.Myanmar},
+ { "Nand", Script.Nandinagari},
+ { "Narb", Script.OldNorthArabian},
+ { "Nbat", Script.Nabataean},
+ { "Newa", Script.Newa},
+ { "Nkoo", Script.Nko},
+ { "Nshu", Script.Nushu},
+ { "Ogam", Script.Ogham},
+ { "Olck", Script.OlChiki},
+ { "Orkh", Script.OldTurkic},
+ { "Orya", Script.Oriya},
+ { "Osge", Script.Osage},
+ { "Osma", Script.Osmanya},
+ { "Palm", Script.Palmyrene},
+ { "Pauc", Script.PauCinHau},
+ { "Perm", Script.OldPermic},
+ { "Phag", Script.PhagsPa},
+ { "Phli", Script.InscriptionalPahlavi},
+ { "Phlp", Script.PsalterPahlavi},
+ { "Phnx", Script.Phoenician},
+ { "Plrd", Script.Miao},
+ { "Prti", Script.InscriptionalParthian},
+ { "Rjng", Script.Rejang},
+ { "Rohg", Script.HanifiRohingya},
+ { "Runr", Script.Runic},
+ { "Samr", Script.Samaritan},
+ { "Sarb", Script.OldSouthArabian},
+ { "Saur", Script.Saurashtra},
+ { "Sgnw", Script.SignWriting},
+ { "Shaw", Script.Shavian},
+ { "Shrd", Script.Sharada},
+ { "Sidd", Script.Siddham},
+ { "Sind", Script.Khudawadi},
+ { "Sinh", Script.Sinhala},
+ { "Sogd", Script.Sogdian},
+ { "Sogo", Script.OldSogdian},
+ { "Sora", Script.SoraSompeng},
+ { "Soyo", Script.Soyombo},
+ { "Sund", Script.Sundanese},
+ { "Sylo", Script.SylotiNagri},
+ { "Syrc", Script.Syriac},
+ { "Tagb", Script.Tagbanwa},
+ { "Takr", Script.Takri},
+ { "Tale", Script.TaiLe},
+ { "Talu", Script.NewTaiLue},
+ { "Taml", Script.Tamil},
+ { "Tang", Script.Tangut},
+ { "Tavt", Script.TaiViet},
+ { "Telu", Script.Telugu},
+ { "Tfng", Script.Tifinagh},
+ { "Tglg", Script.Tagalog},
+ { "Thaa", Script.Thaana},
+ { "Thai", Script.Thai},
+ { "Tibt", Script.Tibetan},
+ { "Tirh", Script.Tirhuta},
+ { "Ugar", Script.Ugaritic},
+ { "Vaii", Script.Vai},
+ { "Wara", Script.WarangCiti},
+ { "Wcho", Script.Wancho},
+ { "Xpeo", Script.OldPersian},
+ { "Xsux", Script.Cuneiform},
+ { "Yezi", Script.Yezidi},
+ { "Yiii", Script.Yi},
+ { "Zanb", Script.ZanabazarSquare},
+ };
+
+ public static Script GetScript(string tag)
+ {
+ if(!s_tagToScript.ContainsKey(tag))
+ {
+ return Script.Unknown;
+ }
+ return s_tagToScript[tag];
+ }
+
+ private static readonly Dictionary s_tagToGeneralCategory =
+ new Dictionary{
+ { "C", GeneralCategory.Other},
+ { "Cc", GeneralCategory.Control},
+ { "Cf", GeneralCategory.Format},
+ { "Cn", GeneralCategory.Unassigned},
+ { "Co", GeneralCategory.PrivateUse},
+ { "Cs", GeneralCategory.Surrogate},
+ { "L", GeneralCategory.Letter},
+ { "LC", GeneralCategory.CasedLetter},
+ { "Ll", GeneralCategory.LowercaseLetter},
+ { "Lm", GeneralCategory.ModifierLetter},
+ { "Lo", GeneralCategory.OtherLetter},
+ { "Lt", GeneralCategory.TitlecaseLetter},
+ { "Lu", GeneralCategory.UppercaseLetter},
+ { "M", GeneralCategory.Mark},
+ { "Mc", GeneralCategory.SpacingMark},
+ { "Me", GeneralCategory.EnclosingMark},
+ { "Mn", GeneralCategory.NonspacingMark},
+ { "N", GeneralCategory.Number},
+ { "Nd", GeneralCategory.DecimalNumber},
+ { "Nl", GeneralCategory.LetterNumber},
+ { "No", GeneralCategory.OtherNumber},
+ { "P", GeneralCategory.Punctuation},
+ { "Pc", GeneralCategory.ConnectorPunctuation},
+ { "Pd", GeneralCategory.DashPunctuation},
+ { "Pe", GeneralCategory.ClosePunctuation},
+ { "Pf", GeneralCategory.FinalPunctuation},
+ { "Pi", GeneralCategory.InitialPunctuation},
+ { "Po", GeneralCategory.OtherPunctuation},
+ { "Ps", GeneralCategory.OpenPunctuation},
+ { "S", GeneralCategory.Symbol},
+ { "Sc", GeneralCategory.CurrencySymbol},
+ { "Sk", GeneralCategory.ModifierSymbol},
+ { "Sm", GeneralCategory.MathSymbol},
+ { "So", GeneralCategory.OtherSymbol},
+ { "Z", GeneralCategory.Separator},
+ { "Zl", GeneralCategory.LineSeparator},
+ { "Zp", GeneralCategory.ParagraphSeparator},
+ { "Zs", GeneralCategory.SpaceSeparator},
+ };
+
+ public static GeneralCategory GetGeneralCategory(string tag)
+ {
+ if(!s_tagToGeneralCategory.ContainsKey(tag))
+ {
+ return GeneralCategory.Other;
+ }
+ return s_tagToGeneralCategory[tag];
+ }
+
+ private static readonly Dictionary s_tagToLineBreakClass =
+ new Dictionary{
+ { "OP", LineBreakClass.OpenPunctuation},
+ { "CL", LineBreakClass.ClosePunctuation},
+ { "CP", LineBreakClass.CloseParenthesis},
+ { "QU", LineBreakClass.Quotation},
+ { "GL", LineBreakClass.Glue},
+ { "NS", LineBreakClass.Nonstarter},
+ { "EX", LineBreakClass.Exclamation},
+ { "SY", LineBreakClass.BreakSymbols},
+ { "IS", LineBreakClass.InfixNumeric},
+ { "PR", LineBreakClass.PrefixNumeric},
+ { "PO", LineBreakClass.PostfixNumeric},
+ { "NU", LineBreakClass.Numeric},
+ { "AL", LineBreakClass.Alphabetic},
+ { "HL", LineBreakClass.HebrewLetter},
+ { "ID", LineBreakClass.Ideographic},
+ { "IN", LineBreakClass.Inseparable},
+ { "HY", LineBreakClass.Hyphen},
+ { "BA", LineBreakClass.BreakAfter},
+ { "BB", LineBreakClass.BreakBefore},
+ { "B2", LineBreakClass.BreakBoth},
+ { "ZW", LineBreakClass.ZWSpace},
+ { "CM", LineBreakClass.CombiningMark},
+ { "WJ", LineBreakClass.WordJoiner},
+ { "H2", LineBreakClass.H2},
+ { "H3", LineBreakClass.H3},
+ { "JL", LineBreakClass.JL},
+ { "JV", LineBreakClass.JV},
+ { "JT", LineBreakClass.JT},
+ { "RI", LineBreakClass.RegionalIndicator},
+ { "EB", LineBreakClass.EBase},
+ { "EM", LineBreakClass.EModifier},
+ { "ZWJ", LineBreakClass.ZWJ},
+ { "CB", LineBreakClass.ContingentBreak},
+ { "XX", LineBreakClass.Unknown},
+ { "AI", LineBreakClass.Ambiguous},
+ { "BK", LineBreakClass.MandatoryBreak},
+ { "CJ", LineBreakClass.ConditionalJapaneseStarter},
+ { "CR", LineBreakClass.CarriageReturn},
+ { "LF", LineBreakClass.LineFeed},
+ { "NL", LineBreakClass.NextLine},
+ { "SA", LineBreakClass.ComplexContext},
+ { "SG", LineBreakClass.Surrogate},
+ { "SP", LineBreakClass.Space},
+ };
+
+ public static LineBreakClass GetLineBreakClass(string tag)
+ {
+ if(!s_tagToLineBreakClass.ContainsKey(tag))
+ {
+ return LineBreakClass.Unknown;
+ }
+ return s_tagToLineBreakClass[tag];
+ }
+
+ private static readonly Dictionary s_tagToBiDiPairedBracketType =
+ new Dictionary{
+ { "n", BidiPairedBracketType.None},
+ { "c", BidiPairedBracketType.Close},
+ { "o", BidiPairedBracketType.Open},
+ };
+
+ public static BidiPairedBracketType GetBiDiPairedBracketType(string tag)
+ {
+ if(!s_tagToBiDiPairedBracketType.ContainsKey(tag))
+ {
+ return BidiPairedBracketType.None;
+ }
+ return s_tagToBiDiPairedBracketType[tag];
+ }
+
+ private static readonly Dictionary s_tagToBiDiClass =
+ new Dictionary{
+ { "L", BidiClass.LeftToRight},
+ { "AL", BidiClass.ArabicLetter},
+ { "AN", BidiClass.ArabicNumber},
+ { "B", BidiClass.ParagraphSeparator},
+ { "BN", BidiClass.BoundaryNeutral},
+ { "CS", BidiClass.CommonSeparator},
+ { "EN", BidiClass.EuropeanNumber},
+ { "ES", BidiClass.EuropeanSeparator},
+ { "ET", BidiClass.EuropeanTerminator},
+ { "FSI", BidiClass.FirstStrongIsolate},
+ { "LRE", BidiClass.LeftToRightEmbedding},
+ { "LRI", BidiClass.LeftToRightIsolate},
+ { "LRO", BidiClass.LeftToRightOverride},
+ { "NSM", BidiClass.NonspacingMark},
+ { "ON", BidiClass.OtherNeutral},
+ { "PDF", BidiClass.PopDirectionalFormat},
+ { "PDI", BidiClass.PopDirectionalIsolate},
+ { "R", BidiClass.RightToLeft},
+ { "RLE", BidiClass.RightToLeftEmbedding},
+ { "RLI", BidiClass.RightToLeftIsolate},
+ { "RLO", BidiClass.RightToLeftOverride},
+ { "S", BidiClass.SegmentSeparator},
+ { "WS", BidiClass.WhiteSpace},
+ };
+
+ public static BidiClass GetBiDiClass(string tag)
+ {
+ if(!s_tagToBiDiClass.ContainsKey(tag))
+ {
+ return BidiClass.LeftToRight;
+ }
+ return s_tagToBiDiClass[tag];
+ }
+
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs
index 4189b24af68..471cb52bea5 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs
@@ -1,4 +1,6 @@
-namespace Avalonia.Media.TextFormatting.Unicode
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Media.TextFormatting.Unicode
{
///
/// Helper for looking up unicode character class information
@@ -7,25 +9,35 @@ internal static class UnicodeData
{
internal const int CATEGORY_BITS = 6;
internal const int SCRIPT_BITS = 8;
- internal const int BIDI_BITS = 5;
internal const int LINEBREAK_BITS = 6;
- internal const int SCRIPT_SHIFT = CATEGORY_BITS;
- internal const int BIDI_SHIFT = CATEGORY_BITS + SCRIPT_BITS;
- internal const int LINEBREAK_SHIFT = CATEGORY_BITS + SCRIPT_BITS + BIDI_BITS;
+ internal const int BIDIPAIREDBRACKED_BITS = 16;
+ internal const int BIDIPAIREDBRACKEDTYPE_BITS = 2;
+ internal const int BIDICLASS_BITS = 5;
+ internal const int SCRIPT_SHIFT = CATEGORY_BITS;
+ internal const int LINEBREAK_SHIFT = CATEGORY_BITS + SCRIPT_BITS;
+
+ internal const int BIDIPAIREDBRACKEDTYPE_SHIFT = BIDIPAIREDBRACKED_BITS;
+ internal const int BIDICLASS_SHIFT = BIDIPAIREDBRACKED_BITS + BIDIPAIREDBRACKEDTYPE_BITS;
+
internal const int CATEGORY_MASK = (1 << CATEGORY_BITS) - 1;
internal const int SCRIPT_MASK = (1 << SCRIPT_BITS) - 1;
- internal const int BIDI_MASK = (1 << BIDI_BITS) - 1;
internal const int LINEBREAK_MASK = (1 << LINEBREAK_BITS) - 1;
+
+ internal const int BIDIPAIREDBRACKED_MASK = (1 << BIDIPAIREDBRACKED_BITS) - 1;
+ internal const int BIDIPAIREDBRACKEDTYPE_MASK = (1 << BIDIPAIREDBRACKEDTYPE_BITS) - 1;
+ internal const int BIDICLASS_MASK = (1 << BIDICLASS_BITS) - 1;
private static readonly UnicodeTrie s_unicodeDataTrie;
private static readonly UnicodeTrie s_graphemeBreakTrie;
+ private static readonly UnicodeTrie s_biDiTrie;
static UnicodeData()
{
s_unicodeDataTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.UnicodeData.trie")!);
s_graphemeBreakTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.GraphemeBreak.trie")!);
+ s_biDiTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.BiDi.trie")!);
}
///
@@ -33,11 +45,10 @@ static UnicodeData()
///
/// The codepoint in question.
/// The code point's general category.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GeneralCategory GetGeneralCategory(int codepoint)
{
- var value = s_unicodeDataTrie.Get(codepoint);
-
- return (GeneralCategory)(value & CATEGORY_MASK);
+ return (GeneralCategory)(s_unicodeDataTrie.Get(codepoint) & CATEGORY_MASK);
}
///
@@ -45,23 +56,43 @@ public static GeneralCategory GetGeneralCategory(int codepoint)
///
/// The codepoint in question.
/// The code point's script.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Script GetScript(int codepoint)
{
- var value = s_unicodeDataTrie.Get(codepoint);
-
- return (Script)((value >> SCRIPT_SHIFT) & SCRIPT_MASK);
+ return (Script)((s_unicodeDataTrie.Get(codepoint) >> SCRIPT_SHIFT) & SCRIPT_MASK);
}
///
- /// Gets the for a Unicode codepoint.
+ /// Gets the for a Unicode codepoint.
///
/// The codepoint in question.
/// The code point's biDi class.
- public static BiDiClass GetBiDiClass(int codepoint)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BidiClass GetBiDiClass(int codepoint)
{
- var value = s_unicodeDataTrie.Get(codepoint);
-
- return (BiDiClass)((value >> BIDI_SHIFT) & BIDI_MASK);
+ return (BidiClass)((s_biDiTrie.Get(codepoint) >> BIDICLASS_SHIFT) & BIDICLASS_MASK);
+ }
+
+ ///
+ /// Gets the for a Unicode codepoint.
+ ///
+ /// The codepoint in question.
+ /// The code point's paired bracket type.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BidiPairedBracketType GetBiDiPairedBracketType(int codepoint)
+ {
+ return (BidiPairedBracketType)((s_biDiTrie.Get(codepoint) >> BIDIPAIREDBRACKEDTYPE_SHIFT) & BIDIPAIREDBRACKEDTYPE_MASK);
+ }
+
+ ///
+ /// Gets the paired bracket for a Unicode codepoint.
+ ///
+ /// The codepoint in question.
+ /// The code point's paired bracket.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Codepoint GetBiDiPairedBracket(int codepoint)
+ {
+ return new Codepoint((int)(s_biDiTrie.Get(codepoint) & BIDIPAIREDBRACKED_MASK));
}
///
@@ -69,11 +100,10 @@ public static BiDiClass GetBiDiClass(int codepoint)
///
/// The codepoint in question.
/// The code point's line break class.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static LineBreakClass GetLineBreakClass(int codepoint)
{
- var value = s_unicodeDataTrie.Get(codepoint);
-
- return (LineBreakClass)((value >> LINEBREAK_SHIFT) & LINEBREAK_MASK);
+ return (LineBreakClass)((s_unicodeDataTrie.Get(codepoint) >> LINEBREAK_SHIFT) & LINEBREAK_MASK);
}
///
@@ -81,6 +111,7 @@ public static LineBreakClass GetLineBreakClass(int codepoint)
///
/// The codepoint in question.
/// The code point's grapheme break type.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphemeBreakClass GetGraphemeClusterBreak(int codepoint)
{
return (GraphemeBreakClass)s_graphemeBreakTrie.Get(codepoint);
diff --git a/src/Avalonia.Visuals/Media/TextHitTestResult.cs b/src/Avalonia.Visuals/Media/TextHitTestResult.cs
index 537dfed49d3..c8922f06c8e 100644
--- a/src/Avalonia.Visuals/Media/TextHitTestResult.cs
+++ b/src/Avalonia.Visuals/Media/TextHitTestResult.cs
@@ -1,23 +1,38 @@
+using Avalonia.Media.TextFormatting;
+
namespace Avalonia.Media
{
///
- /// Holds a hit test result from a .
+ /// Holds a hit test result from a .
///
- public class TextHitTestResult
+ public readonly struct TextHitTestResult
{
+ public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing)
+ {
+ CharacterHit = characterHit;
+ TextPosition = textPosition;
+ IsInside = isInside;
+ IsTrailing = isTrailing;
+ }
+
+ ///
+ /// Gets the character hit of the hit test result.
+ ///
+ public CharacterHit CharacterHit { get; }
+
///
- /// Gets or sets a value indicating whether the point is inside the bounds of the text.
+ /// Gets a value indicating whether the point is inside the bounds of the text.
///
- public bool IsInside { get; set; }
+ public bool IsInside { get; }
///
/// Gets the index of the hit character in the text.
///
- public int TextPosition { get; set; }
+ public int TextPosition { get; }
///
/// Gets a value indicating whether the hit is on the trailing edge of the character.
///
- public bool IsTrailing { get; set; }
+ public bool IsTrailing { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs
index 8245b634409..45540a58123 100644
--- a/src/Avalonia.Visuals/Media/Typeface.cs
+++ b/src/Avalonia.Visuals/Media/Typeface.cs
@@ -48,7 +48,7 @@ public Typeface(string fontFamilyName,
///
/// Gets the font family.
///
- public FontFamily? FontFamily { get; }
+ public FontFamily FontFamily { get; }
///
/// Gets the font style.
diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
index a1f42c171c9..82af6ff0b66 100644
--- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
@@ -83,13 +83,6 @@ void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
///
void DrawEllipse(IBrush? brush, IPen? pen, Rect rect);
- ///
- /// Draws text.
- ///
- /// The foreground brush.
- /// The upper-left corner of the text.
- /// The text.
- void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
///
/// Draws a glyph run.
diff --git a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs
deleted file mode 100644
index 330fcac50cf..00000000000
--- a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Media;
-
-namespace Avalonia.Platform
-{
- ///
- /// Defines the platform-specific interface for .
- ///
- public interface IFormattedTextImpl
- {
- ///
- /// Gets the constraint of the text.
- ///
- Size Constraint { get; }
-
- ///
- /// The measured bounds of the text.
- ///
- Rect Bounds{ get; }
-
- ///
- /// Gets the text.
- ///
- string Text { get; }
-
- ///
- /// Gets the lines in the text.
- ///
- ///
- /// A collection of objects.
- ///
- IEnumerable GetLines();
-
- ///
- /// Hit tests a point in the text.
- ///
- /// The point.
- ///
- /// A describing the result of the hit test.
- ///
- TextHitTestResult HitTestPoint(Point point);
-
- ///
- /// Gets the bounds rectangle that the specified character occupies.
- ///
- /// The index of the character.
- /// The character bounds.
- Rect HitTestTextPosition(int index);
-
- ///
- /// Gets the bounds rectangles that the specified text range occupies.
- ///
- /// The index of the first character.
- /// The number of characters in the text range.
- /// The character bounds.
- IEnumerable HitTestTextRange(int index, int length);
- }
-}
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index 60ae0b5ef8e..a295a8cdc97 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -11,27 +11,6 @@ namespace Avalonia.Platform
///
public interface IPlatformRenderInterface
{
- ///
- /// Creates a formatted text implementation.
- ///
- /// The text.
- /// The base typeface.
- /// The font size.
- /// The text alignment.
- /// The text wrapping mode.
- /// The text layout constraints.
- /// The style spans.
- /// An .
- IFormattedTextImpl CreateFormattedText(
- string text,
- Typeface typeface,
- double fontSize,
- TextAlignment textAlignment,
- TextWrapping wrapping,
- Size constraint,
- IReadOnlyList? spans);
-
- ///
/// Creates an ellipse geometry implementation.
///
/// The bounds of the ellipse.
diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
index 73d198d7ef5..aced05c9d87 100644
--- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
+++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
@@ -1,5 +1,6 @@
using System.Globalization;
using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Platform
@@ -10,13 +11,14 @@ namespace Avalonia.Platform
public interface ITextShaperImpl
{
///
- /// Shapes the specified region within the text and returns a resulting glyph run.
+ /// Shapes the specified region within the text and returns a shaped buffer.
///
/// The text.
/// The typeface.
/// The font rendering em size.
/// The culture.
+ /// The bidi level.
/// A shaped glyph run.
- GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo? culture);
+ ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, sbyte bidiLevel);
}
}
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index c453181f65b..82be0a1a0f4 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
@@ -588,7 +588,10 @@ private void RenderComposite(Scene scene, ref IDrawingContextImpl? context)
if (DrawFps)
{
- RenderFps(context, clientRect, scene.Layers.Count);
+ using (var c = new DrawingContext(context, false))
+ {
+ RenderFps(c, clientRect, scene.Layers.Count);
+ }
}
}
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index f6642102f76..23016de148d 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -78,7 +78,7 @@ public void Paint(Rect rect)
if (DrawFps)
{
- RenderFps(context.PlatformImpl, _root.Bounds, null);
+ RenderFps(context, _root.Bounds, null);
}
}
}
diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs
index 5c9cace4cd4..90ba60c42aa 100644
--- a/src/Avalonia.Visuals/Rendering/RendererBase.cs
+++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs
@@ -1,7 +1,7 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using Avalonia.Media;
-using Avalonia.Platform;
namespace Avalonia.Rendering
{
@@ -12,22 +12,16 @@ public class RendererBase
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
private int _fps;
- private FormattedText _fpsText;
private TimeSpan _lastFpsUpdate;
public RendererBase(bool useManualFpsCounting = false)
{
_useManualFpsCounting = useManualFpsCounting;
- _fpsText = new FormattedText
- {
- Typeface = new Typeface(FontFamily.Default),
- FontSize = s_fontSize
- };
}
protected void FpsTick() => _framesThisSecond++;
- protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount)
+ protected void RenderFps(DrawingContext context, Rect clientRect, int? layerCount)
{
var now = _stopwatch.Elapsed;
var elapsed = now - _lastFpsUpdate;
@@ -42,21 +36,15 @@ protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? laye
_lastFpsUpdate = now;
}
- if (layerCount.HasValue)
- {
- _fpsText.Text = string.Format("Layers: {0} FPS: {1:000}", layerCount, _fps);
- }
- else
- {
- _fpsText.Text = string.Format("FPS: {0:000}", _fps);
- }
+ var text = layerCount.HasValue ? $"Layers: {layerCount} FPS: {_fps:000}" : $"FPS: {_fps:000}";
+
+ var formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, s_fontSize, Brushes.White);
+
+ var rect = new Rect(clientRect.Right - formattedText.Width, 0, formattedText.Width, formattedText.Height);
- var size = _fpsText.Bounds.Size;
- var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
+ context.DrawRectangle(Brushes.Black, null, rect);
- context.Transform = Matrix.Identity;
- context.DrawRectangle(Brushes.Black,null, rect);
- context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl);
+ context.DrawText(formattedText, rect.TopLeft);
}
}
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index 688cbd83c85..9710ca6c3c4 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -202,21 +202,6 @@ public void Custom(ICustomDrawOperation custom)
++_drawOperationindex;
}
- ///
- public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
- {
- var next = NextDrawAs();
-
- if (next == null || !next.Item.Equals(Transform, foreground, origin, text))
- {
- Add(new TextNode(Transform, foreground, origin, text, CreateChildScene(foreground)));
- }
- else
- {
- ++_drawOperationindex;
- }
- }
-
///
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
deleted file mode 100644
index 4a1587fb900..00000000000
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using System.Collections.Generic;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.VisualTree;
-
-namespace Avalonia.Rendering.SceneGraph
-{
- ///
- /// A node in the scene graph which represents a text draw.
- ///
- internal class TextNode : BrushDrawOperation
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The transform.
- /// The foreground brush.
- /// The draw origin.
- /// The text to draw.
- /// Child scenes for drawing visual brushes.
- public TextNode(
- Matrix transform,
- IBrush foreground,
- Point origin,
- IFormattedTextImpl text,
- IDictionary? childScenes = null)
- : base(text.Bounds.Translate(origin), transform)
- {
- Transform = transform;
- Foreground = foreground.ToImmutable();
- Origin = origin;
- Text = text;
- ChildScenes = childScenes;
- }
-
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
-
- ///
- /// Gets the foreground brush.
- ///
- public IBrush Foreground { get; }
-
- ///
- /// Gets the draw origin.
- ///
- public Point Origin { get; }
-
- ///
- /// Gets the text to draw.
- ///
- public IFormattedTextImpl Text { get; }
-
- ///
- public override IDictionary? ChildScenes { get; }
-
- ///
- public override void Render(IDrawingContextImpl context)
- {
- context.Transform = Transform;
- context.DrawText(Foreground, Origin, Text);
- }
-
- ///
- /// Determines if this draw operation equals another.
- ///
- /// The transform of the other draw operation.
- /// The foreground of the other draw operation.
- /// The draw origin of the other draw operation.
- /// The text of the other draw operation.
- /// True if the draw operations are the same, otherwise false.
- ///
- /// The properties of the other draw operation are passed in as arguments to prevent
- /// allocation of a not-yet-constructed draw operation object.
- ///
- internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text)
- {
- return transform == Transform &&
- Equals(foreground, Foreground) &&
- origin == Origin &&
- Equals(text, Text);
- }
-
- ///
- public override bool HitTest(Point p) => Bounds.Contains(p);
- }
-}
diff --git a/src/Avalonia.Visuals/Utilities/ArrayBuilder.cs b/src/Avalonia.Visuals/Utilities/ArrayBuilder.cs
new file mode 100644
index 00000000000..70486594314
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/ArrayBuilder.cs
@@ -0,0 +1,184 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+ ///
+ /// A helper type for avoiding allocations while building arrays.
+ ///
+ /// The type of item contained in the array.
+ internal struct ArrayBuilder
+ where T : struct
+ {
+ private const int DefaultCapacity = 4;
+ private const int MaxCoreClrArrayLength = 0x7FeFFFFF;
+
+ // Starts out null, initialized on first Add.
+ private T[] _data;
+ private int _size;
+
+ ///
+ /// Gets or sets the number of items in the array.
+ ///
+ public int Length
+ {
+ get => _size;
+
+ set
+ {
+ if (value == _size)
+ {
+ return;
+ }
+
+ if (value > 0)
+ {
+ EnsureCapacity(value);
+
+ _size = value;
+ }
+ else
+ {
+ _size = 0;
+ }
+ }
+ }
+
+ ///
+ /// Returns a reference to specified element of the array.
+ ///
+ /// The index of the element to return.
+ /// The .
+ ///
+ /// Thrown when index less than 0 or index greater than or equal to .
+ ///
+ public ref T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+#if DEBUG
+ if (index.CompareTo(0) < 0 || index.CompareTo(_size) > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+#endif
+
+ return ref _data![index];
+ }
+ }
+
+ ///
+ /// Appends a given number of empty items to the array returning
+ /// the items as a slice.
+ ///
+ /// The number of items in the slice.
+ /// Whether to clear the new slice, Defaults to .
+ /// The .
+ public ArraySlice Add(int length, bool clear = true)
+ {
+ var position = _size;
+
+ // Expand the array.
+ Length += length;
+
+ var slice = AsSlice(position, Length - position);
+
+ if (clear)
+ {
+ slice.Span.Clear();
+ }
+
+ return slice;
+ }
+
+ ///
+ /// Appends the slice to the array copying the data across.
+ ///
+ /// The array slice.
+ /// The .
+ public ArraySlice Add(in ReadOnlySlice value)
+ {
+ var position = _size;
+
+ // Expand the array.
+ Length += value.Length;
+
+ var slice = AsSlice(position, Length - position);
+
+ value.Span.CopyTo(slice.Span);
+
+ return slice;
+ }
+
+ ///
+ /// 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.
+ _size = 0;
+ }
+
+ private void EnsureCapacity(int min)
+ {
+ var length = _data?.Length ?? 0;
+
+ if (length >= min)
+ {
+ return;
+ }
+
+ // Same expansion algorithm as List.
+ var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u;
+
+ if (newCapacity > MaxCoreClrArrayLength)
+ {
+ newCapacity = MaxCoreClrArrayLength;
+ }
+
+ if (newCapacity < min)
+ {
+ newCapacity = (uint)min;
+ }
+
+ var array = new T[newCapacity];
+
+ if (_size > 0)
+ {
+ Array.Copy(_data!, array, _size);
+ }
+
+ _data = array;
+ }
+
+ ///
+ /// Returns the current state of the array as a slice.
+ ///
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ArraySlice AsSlice() => AsSlice(Length);
+
+ ///
+ /// Returns the current state of the array as a slice.
+ ///
+ /// The number of items in the slice.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ArraySlice AsSlice(int length) => new ArraySlice(_data!, 0, length);
+
+ ///
+ /// Returns the current state of the array as a slice.
+ ///
+ /// The index at which to begin the slice.
+ /// The number of items in the slice.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length);
+ }
+}
diff --git a/src/Avalonia.Visuals/Utilities/ArraySlice.cs b/src/Avalonia.Visuals/Utilities/ArraySlice.cs
new file mode 100644
index 00000000000..f5a9d3a98d3
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/ArraySlice.cs
@@ -0,0 +1,197 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+ ///
+ /// ArraySlice represents a contiguous region of arbitrary memory similar
+ /// to and though constrained
+ /// to arrays.
+ /// Unlike , it is not a byref-like type.
+ ///
+ /// The type of item contained in the slice.
+ internal readonly struct ArraySlice : IReadOnlyList
+ where T : struct
+ {
+ ///
+ /// Gets an empty
+ ///
+ public static ArraySlice Empty => new ArraySlice(Array.Empty());
+
+ private readonly T[] _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data buffer.
+ public ArraySlice(T[] data)
+ : this(data, 0, data.Length)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data buffer.
+ /// The offset position in the underlying buffer this slice was created from.
+ /// The number of items in the slice.
+ public ArraySlice(T[] data, int start, int length)
+ {
+#if DEBUG
+ if (start.CompareTo(0) < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start));
+ }
+
+ if (length.CompareTo(data.Length) > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ if ((start + length).CompareTo(data.Length) > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(data));
+ }
+#endif
+
+ _data = data;
+ Start = start;
+ Length = length;
+ }
+
+
+ ///
+ /// Gets a value that indicates whether this instance of is Empty.
+ ///
+ public bool IsEmpty => Length == 0;
+
+ ///
+ /// Gets the offset position in the underlying buffer this slice was created from.
+ ///
+ public int Start { get; }
+
+ ///
+ /// Gets the number of items in the slice.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets a representing this slice.
+ ///
+ public Span Span => new Span(_data, Start, Length);
+
+ ///
+ /// Returns a reference to specified element of the slice.
+ ///
+ /// The index of the element to return.
+ /// The .
+ ///
+ /// Thrown when index less than 0 or index greater than or equal to .
+ ///
+ public ref T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+#if DEBUG
+ if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+#endif
+ var i = index + Start;
+
+ return ref _data[i];
+ }
+ }
+
+ ///
+ /// Defines an implicit conversion of a to a
+ ///
+ public static implicit operator ReadOnlySlice(ArraySlice slice)
+ {
+ return new ReadOnlySlice(slice._data).AsSlice(slice.Start, slice.Length);
+ }
+
+ ///
+ /// Defines an implicit conversion of an array to a
+ ///
+ public static implicit operator ArraySlice(T[] array) => new ArraySlice(array, 0, array.Length);
+
+ ///
+ /// Fills the contents of this slice with the given value.
+ ///
+ public void Fill(T value) => Span.Fill(value);
+
+ ///
+ /// Forms a slice out of the given slice, beginning at 'start', of given length
+ ///
+ /// The index at which to begin this slice.
+ /// The desired length for the slice (exclusive).
+ ///
+ /// Thrown when the specified or end index is not in range (<0 or >Length).
+ ///
+ public ArraySlice Slice(int start, int length) => new ArraySlice(_data, start, length);
+
+ ///
+ /// Returns a specified number of contiguous elements from the start of the slice.
+ ///
+ /// The number of elements to return.
+ /// A that contains the specified number of elements from the start of this slice.
+ public ArraySlice Take(int length)
+ {
+ if (IsEmpty)
+ {
+ return this;
+ }
+
+ if (length > Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ArraySlice(_data, Start, length);
+ }
+
+ ///
+ /// Bypasses a specified number of elements in the slice and then returns the remaining elements.
+ ///
+ /// The number of elements to skip before returning the remaining elements.
+ /// A that contains the elements that occur after the specified index in this slice.
+ public ArraySlice Skip(int length)
+ {
+ if (IsEmpty)
+ {
+ return this;
+ }
+
+ if (length > Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ArraySlice(_data, Start + length, Length - length);
+ }
+
+ public ImmutableReadOnlyListStructEnumerator GetEnumerator() =>
+ new ImmutableReadOnlyListStructEnumerator(this);
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ T IReadOnlyList.this[int index] => this[index];
+
+ ///
+ int IReadOnlyCollection.Count => Length;
+ }
+}
diff --git a/src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs b/src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs
new file mode 100644
index 00000000000..a4f6ae89c14
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs
@@ -0,0 +1,80 @@
+// RichTextKit
+// Copyright © 2019-2020 Topten Software. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may
+// not use this product except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+// Copied from: https://github.com/toptensoftware/RichTextKit
+
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Utilities
+{
+ ///
+ /// Extension methods for binary searching an IReadOnlyList collection
+ ///
+ internal static class BinarySearchExtension
+ {
+ private static int GetMedian(int low, int hi)
+ {
+ System.Diagnostics.Debug.Assert(low <= hi);
+ System.Diagnostics.Debug.Assert(hi - low >= 0, "Length overflow!");
+ return low + (hi - low >> 1);
+ }
+
+ ///
+ /// Performs a binary search on the entire contents of an IReadOnlyList
+ ///
+ /// The list element type
+ /// The list to be searched
+ /// 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
+ {
+ return list.BinarySearch(0, list.Count, value, comparer);
+ }
+
+ ///
+ /// Performs a binary search on a a subset of an IReadOnlyList
+ ///
+ /// The list element type
+ /// The list to be searched
+ /// The start of the range to be searched
+ /// The length of the range to be searched
+ /// The value to search for
+ /// A 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, int index, int length, T value, IComparer comparer)
+ {
+ // Based on this: https://referencesource.microsoft.com/#mscorlib/system/array.cs,957
+ var lo = index;
+ var hi = index + length - 1;
+ while (lo <= hi)
+ {
+ var i = GetMedian(lo, hi);
+ var c = comparer.Compare(list[i], value);
+ if (c == 0)
+ return i;
+ if (c < 0)
+ {
+ lo = i + 1;
+ }
+ else
+ {
+ hi = i - 1;
+ }
+ }
+ return ~lo;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Utilities/FrugalList.cs b/src/Avalonia.Visuals/Utilities/FrugalList.cs
new file mode 100644
index 00000000000..3ef74185338
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/FrugalList.cs
@@ -0,0 +1,2360 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+// These classes implement a frugal storage model for lists of type .
+// Performance measurements show that Avalon has many lists that contain
+// a limited number of entries, and frequently zero or a single entry.
+// Therefore these classes are structured to prefer a storage model that
+// starts at zero, and employs a conservative growth strategy to minimize
+// the steady state memory footprint. Also note that the list uses one
+// fewer objects than ArrayList and List and does no allocations at all
+// until an item is inserted into the list.
+//
+// The code is also structured to perform well from a CPU standpoint. Perf
+// analysis shows that the reduced number of processor cache misses makes
+// FrugalList faster than ArrayList or List, especially for lists of 6
+// or fewer items. Timing differ with the size of .
+//
+// FrugalList is appropriate for small lists or lists that grow slowly.
+// Due to the slow growth, if you use it for a list with more than 6 initial
+// entries is best to set the capacity of the list at construction time or
+// soon after. If you must grow the list by a large amount, set the capacity
+// or use Insert() method to force list growth to the final size. Choose
+// your collections wisely and pay particular attention to the growth
+// patterns and search methods.
+
+// FrugalList has all of the methods of the IList interface, but implements
+// them as virtual methods of the class and not as Interface methods. This
+// is to avoid the virtual stub dispatch CPU costs associated with Interfaces.
+
+namespace Avalonia.Utilities
+{
+ // These classes implement a frugal storage model for lists of .
+ // Performance measurements show that many lists contain a single item.
+ // Therefore this list is structured to prefer a list that contains a single
+ // item, and does conservative growth to minimize the memory footprint.
+
+ // This enum controls the growth to successively more complex storage models
+ internal enum FrugalListStoreState
+ {
+ Success,
+ SingleItemList,
+ ThreeItemList,
+ SixItemList,
+ Array
+ }
+
+#nullable disable
+ internal abstract class FrugalListBase
+ {
+ ///
+ /// Number of entries in this store
+ ///
+ // Number of entries in this store
+ public int Count
+ {
+ get
+ {
+ return _count;
+ }
+ }
+
+ // for use only by trusted callers - e.g. FrugalObjectList.Compacter
+ internal void TrustedSetCount(int newCount)
+ {
+ _count = newCount;
+ }
+
+ ///
+ /// Capacity of this store
+ ///
+ public abstract int Capacity
+ {
+ get;
+ }
+
+ // Increase size if needed, insert item into the store
+ public abstract FrugalListStoreState Add(T value);
+
+ ///
+ /// Removes all values from the store
+ ///
+ public abstract void Clear();
+
+ ///
+ /// Returns true if the store contains the entry.
+ ///
+ public abstract bool Contains(T value);
+
+ ///
+ /// Returns the index into the store that contains the item.
+ /// -1 is returned if the item is not in the store.
+ ///
+ public abstract int IndexOf(T value);
+
+ ///
+ /// Insert item into the store at index, grows if needed
+ ///
+ public abstract void Insert(int index, T value);
+
+ // Place item into the store at index
+ public abstract void SetAt(int index, T value);
+
+ ///
+ /// Removes the item from the store. If the item was not
+ /// in the store false is returned.
+ ///
+ public abstract bool Remove(T value);
+
+ ///
+ /// Removes the item from the store
+ ///
+ public abstract void RemoveAt(int index);
+
+ ///
+ /// Return the item at index in the store
+ ///
+ public abstract T EntryAt(int index);
+
+ ///
+ /// Promotes the values in the current store to the next larger
+ /// and more complex storage model.
+ ///
+ public abstract void Promote(FrugalListBase newList);
+
+ ///
+ /// Returns the entries as an array
+ ///
+ public abstract T[] ToArray();
+
+ ///
+ /// Copies the entries to the given array starting at the
+ /// specified index
+ ///
+ public abstract void CopyTo(T[] array, int index);
+
+ ///
+ /// Creates a shallow copy of the list
+ ///
+ public abstract object Clone();
+
+ // The number of items in the list.
+ protected int _count;
+
+ public virtual Compacter NewCompacter(int newCount)
+ {
+ return new Compacter(this, newCount);
+ }
+
+ // basic implementation - compacts in-place
+ internal class Compacter
+ {
+ protected readonly FrugalListBase _store;
+ private readonly int _newCount;
+
+ protected int _validItemCount;
+ protected int _previousEnd;
+
+ public Compacter(FrugalListBase store, int newCount)
+ {
+ _store = store;
+ _newCount = newCount;
+ }
+
+ public void Include(int start, int end)
+ {
+ Debug.Assert(start >= _previousEnd, "Arguments out of order during Compact");
+ Debug.Assert(_validItemCount + end - start <= _newCount, "Too many items copied during Compact");
+
+ IncludeOverride(start, end);
+
+ _previousEnd = end;
+ }
+
+ protected virtual void IncludeOverride(int start, int end)
+ {
+ // item-by-item move
+ for (var i = start; i < end; ++i)
+ {
+ _store.SetAt(_validItemCount++, _store.EntryAt(i));
+ }
+ }
+
+ public virtual FrugalListBase Finish()
+ {
+ // clear out vacated entries
+ var filler = default(T);
+
+ for (int i = _validItemCount, n = _store._count; i < n; ++i)
+ {
+ _store.SetAt(i, filler);
+ }
+
+ _store._count = _validItemCount;
+
+ return _store;
+ }
+ }
+ }
+
+ ///
+ /// A simple class to handle a single item
+ ///
+ internal sealed class SingleItemList : FrugalListBase
+ {
+ private const int SIZE = 1;
+
+ private T _loneEntry;
+
+ // Capacity of this store
+ public override int Capacity
+ {
+ get
+ {
+ return SIZE;
+ }
+ }
+
+ public override FrugalListStoreState Add(T value)
+ {
+ // If we don't have any entries or the existing entry is being overwritten,
+ // then we can use this store. Otherwise we have to promote.
+ if (0 == _count)
+ {
+ _loneEntry = value;
+ ++_count;
+ return FrugalListStoreState.Success;
+ }
+ else
+ {
+ // Entry already used, move to an ThreeItemList
+ return FrugalListStoreState.ThreeItemList;
+ }
+ }
+
+ public override void Clear()
+ {
+ // Wipe out the info
+ _loneEntry = default;
+ _count = 0;
+ }
+
+ public override bool Contains(T value)
+ {
+ return _loneEntry.Equals(value);
+ }
+
+ public override int IndexOf(T value)
+ {
+ if (_loneEntry.Equals(value))
+ {
+ return 0;
+ }
+ return -1;
+ }
+
+ public override void Insert(int index, T value)
+ {
+ // Should only get here if count and index are 0
+ if (_count < SIZE && index < SIZE)
+ {
+ _loneEntry = value;
+ ++_count;
+ return;
+ }
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public override void SetAt(int index, T value)
+ {
+ // Overwrite item at index
+ _loneEntry = value;
+ }
+
+ public override bool Remove(T value)
+ {
+ // Wipe out the info in the only entry if it matches the item.
+ if (_loneEntry.Equals(value))
+ {
+ _loneEntry = default;
+ --_count;
+ return true;
+ }
+
+ return false;
+ }
+
+ public override void RemoveAt(int index)
+ {
+ // Wipe out the info at index
+ if (0 == index)
+ {
+ _loneEntry = default;
+ --_count;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ public override T EntryAt(int index)
+ {
+ return _loneEntry;
+ }
+
+ public override void Promote(FrugalListBase oldList)
+ {
+ if (SIZE == oldList.Count)
+ {
+ SetCount(SIZE);
+ SetAt(0, oldList.EntryAt(0));
+ }
+ else
+ {
+ // this list is smaller than oldList
+ throw new ArgumentException($"Cannot promote from '{oldList}' to '{ToString()}' because the target map is too small.", nameof(oldList));
+ }
+ }
+
+ // Class specific implementation to avoid virtual method calls and additional logic
+ public void Promote(SingleItemList oldList)
+ {
+ SetCount(oldList.Count);
+ SetAt(0, oldList.EntryAt(0));
+ }
+
+ public override T[] ToArray()
+ {
+ var array = new T[1];
+ array[0] = _loneEntry;
+ return array;
+ }
+
+ public override void CopyTo(T[] array, int index)
+ {
+ array[index] = _loneEntry;
+ }
+
+ public override object Clone()
+ {
+ var newList = new SingleItemList();
+ newList.Promote(this);
+ return newList;
+ }
+
+ private void SetCount(int value)
+ {
+ if (value >= 0 && value <= SIZE)
+ {
+ _count = value;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ }
+ }
+
+
+ ///
+ /// A simple class to handle a list with 3 items. Perf analysis showed
+ /// that this yielded better memory locality and perf than an object and an array.
+ ///
+ internal sealed class ThreeItemList : FrugalListBase
+ {
+ private const int SIZE = 3;
+
+ private T _entry0;
+ private T _entry1;
+ private T _entry2;
+
+ // Capacity of this store
+ public override int Capacity
+ {
+ get
+ {
+ return SIZE;
+ }
+ }
+
+ public override FrugalListStoreState Add(T value)
+ {
+ switch (_count)
+ {
+ case 0:
+ _entry0 = value;
+ break;
+
+ case 1:
+ _entry1 = value;
+ break;
+
+ case 2:
+ _entry2 = value;
+ break;
+
+ default:
+ // We have to promote
+ return FrugalListStoreState.SixItemList;
+ }
+ ++_count;
+ return FrugalListStoreState.Success;
+ }
+
+ public override void Clear()
+ {
+ // Wipe out the info.
+ _entry0 = default;
+ _entry1 = default;
+ _entry2 = default;
+ _count = 0;
+ }
+
+ public override bool Contains(T value)
+ {
+ return -1 != IndexOf(value);
+ }
+
+ public override int IndexOf(T value)
+ {
+ if (_entry0.Equals(value))
+ {
+ return 0;
+ }
+
+ if (_count > 1)
+ {
+ if (_entry1.Equals(value))
+ {
+ return 1;
+ }
+ if (3 == _count && _entry2.Equals(value))
+ {
+ return 2;
+ }
+ }
+
+ return -1;
+ }
+
+ public override void Insert(int index, T value)
+ {
+ // Should only get here if count < SIZE
+ if (_count < SIZE)
+ {
+ switch (index)
+ {
+ case 0:
+ _entry2 = _entry1;
+ _entry1 = _entry0;
+ _entry0 = value;
+ break;
+
+ case 1:
+ _entry2 = _entry1;
+ _entry1 = value;
+ break;
+
+ case 2:
+ _entry2 = value;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ ++_count;
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public override void SetAt(int index, T value)
+ {
+ // Overwrite item at index
+ switch (index)
+ {
+ case 0:
+ _entry0 = value;
+ break;
+
+ case 1:
+ _entry1 = value;
+ break;
+
+ case 2:
+ _entry2 = value;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ public override bool Remove(T value)
+ {
+ // If the item matches an existing entry, wipe out the last
+ // entry and move all the other entries up. Because we only
+ // have three entries we can just unravel all the cases.
+ if (_entry0.Equals(value))
+ {
+ RemoveAt(0);
+ return true;
+ }
+ else if (_count > 1)
+ {
+ if (_entry1.Equals(value))
+ {
+ RemoveAt(1);
+ return true;
+ }
+ else if (3 == _count && _entry2.Equals(value))
+ {
+ RemoveAt(2);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public override void RemoveAt(int index)
+ {
+ // Remove entry at index, wipe out the last entry and move
+ // all the other entries up. Because we only have three
+ // entries we can just unravel all the cases.
+ switch (index)
+ {
+ case 0:
+ _entry0 = _entry1;
+ _entry1 = _entry2;
+ break;
+
+ case 1:
+ _entry1 = _entry2;
+ break;
+
+ case 2:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ _entry2 = default;
+
+ --_count;
+ }
+
+ public override T EntryAt(int index)
+ {
+ switch (index)
+ {
+ case 0:
+ return _entry0;
+
+ case 1:
+ return _entry1;
+
+ case 2:
+ return _entry2;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ public override void Promote(FrugalListBase oldList)
+ {
+ var oldCount = oldList.Count;
+
+ if (SIZE >= oldCount)
+ {
+ SetCount(oldList.Count);
+
+ switch (oldCount)
+ {
+ case 3:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ break;
+
+ case 2:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ break;
+
+ case 1:
+ SetAt(0, oldList.EntryAt(0));
+ break;
+
+ case 0:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(oldList));
+ }
+ }
+ else
+ {
+ // this list is smaller than oldList
+ throw new ArgumentException($"Cannot promote from '{oldList}' to '{ToString()}' because the target map is too small.", nameof(oldList));
+ }
+ }
+
+ // Class specific implementation to avoid virtual method calls and additional logic
+ public void Promote(SingleItemList oldList)
+ {
+ SetCount(oldList.Count);
+ SetAt(0, oldList.EntryAt(0));
+ }
+
+ // Class specific implementation to avoid virtual method calls and additional logic
+ public void Promote(ThreeItemList oldList)
+ {
+ var oldCount = oldList.Count;
+ SetCount(oldList.Count);
+
+ switch (oldCount)
+ {
+ case 3:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ break;
+
+ case 2:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ break;
+
+ case 1:
+ SetAt(0, oldList.EntryAt(0));
+ break;
+
+ case 0:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(oldList));
+ }
+ }
+
+ public override T[] ToArray()
+ {
+ var array = new T[_count];
+
+ array[0] = _entry0;
+ if (_count >= 2)
+ {
+ array[1] = _entry1;
+ if (_count == 3)
+ {
+ array[2] = _entry2;
+ }
+ }
+ return array;
+ }
+
+ public override void CopyTo(T[] array, int index)
+ {
+ array[index] = _entry0;
+ if (_count >= 2)
+ {
+ array[index + 1] = _entry1;
+ if (_count == 3)
+ {
+ array[index + 2] = _entry2;
+ }
+ }
+ }
+
+ public override object Clone()
+ {
+ var newList = new ThreeItemList();
+ newList.Promote(this);
+ return newList;
+ }
+
+ private void SetCount(int value)
+ {
+ if (value >= 0 && value <= SIZE)
+ {
+ _count = value;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ }
+ }
+
+ ///
+ /// A simple class to handle a list with 6 items.
+ ///
+ internal sealed class SixItemList : FrugalListBase
+ {
+ private const int SIZE = 6;
+
+ private T _entry0;
+ private T _entry1;
+ private T _entry2;
+ private T _entry3;
+ private T _entry4;
+ private T _entry5;
+
+ // Capacity of this store
+ public override int Capacity
+ {
+ get
+ {
+ return SIZE;
+ }
+ }
+
+ public override FrugalListStoreState Add(T value)
+ {
+ switch (_count)
+ {
+ case 0:
+ _entry0 = value;
+ break;
+
+ case 1:
+ _entry1 = value;
+ break;
+
+ case 2:
+ _entry2 = value;
+ break;
+
+ case 3:
+ _entry3 = value;
+ break;
+
+ case 4:
+ _entry4 = value;
+ break;
+
+ case 5:
+ _entry5 = value;
+ break;
+
+ default:
+ // We have to promote
+ return FrugalListStoreState.Array;
+ }
+ ++_count;
+ return FrugalListStoreState.Success;
+ }
+
+ public override void Clear()
+ {
+ // Wipe out the info.
+ _entry0 = default;
+ _entry1 = default;
+ _entry2 = default;
+ _entry3 = default;
+ _entry4 = default;
+ _entry5 = default;
+ _count = 0;
+ }
+
+ public override bool Contains(T value)
+ {
+ return -1 != IndexOf(value);
+ }
+
+ public override int IndexOf(T value)
+ {
+ if (_entry0.Equals(value))
+ {
+ return 0;
+ }
+ if (_count > 1)
+ {
+ if (_entry1.Equals(value))
+ {
+ return 1;
+ }
+ if (_count > 2)
+ {
+ if (_entry2.Equals(value))
+ {
+ return 2;
+ }
+ if (_count > 3)
+ {
+ if (_entry3.Equals(value))
+ {
+ return 3;
+ }
+ if (_count > 4)
+ {
+ if (_entry4.Equals(value))
+ {
+ return 4;
+ }
+ if (6 == _count && _entry5.Equals(value))
+ {
+ return 5;
+ }
+ }
+ }
+ }
+ }
+ return -1;
+ }
+
+ public override void Insert(int index, T value)
+ {
+ // Should only get here if count is less than SIZE
+ if (_count < SIZE)
+ {
+ switch (index)
+ {
+ case 0:
+ _entry5 = _entry4;
+ _entry4 = _entry3;
+ _entry3 = _entry2;
+ _entry2 = _entry1;
+ _entry1 = _entry0;
+ _entry0 = value;
+ break;
+
+ case 1:
+ _entry5 = _entry4;
+ _entry4 = _entry3;
+ _entry3 = _entry2;
+ _entry2 = _entry1;
+ _entry1 = value;
+ break;
+
+ case 2:
+ _entry5 = _entry4;
+ _entry4 = _entry3;
+ _entry3 = _entry2;
+ _entry2 = value;
+ break;
+
+ case 3:
+ _entry5 = _entry4;
+ _entry4 = _entry3;
+ _entry3 = value;
+ break;
+
+ case 4:
+ _entry5 = _entry4;
+ _entry4 = value;
+ break;
+
+ case 5:
+ _entry5 = value;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ ++_count;
+ return;
+ }
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public override void SetAt(int index, T value)
+ {
+ // Overwrite item at index
+ switch (index)
+ {
+ case 0:
+ _entry0 = value;
+ break;
+
+ case 1:
+ _entry1 = value;
+ break;
+
+ case 2:
+ _entry2 = value;
+ break;
+
+ case 3:
+ _entry3 = value;
+ break;
+
+ case 4:
+ _entry4 = value;
+ break;
+
+ case 5:
+ _entry5 = value;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ public override bool Remove(T value)
+ {
+ // If the item matches an existing entry, wipe out the last
+ // entry and move all the other entries up. Because we only
+ // have six entries we can just unravel all the cases.
+ if (_entry0.Equals(value))
+ {
+ RemoveAt(0);
+ return true;
+ }
+ else if (_count > 1)
+ {
+ if (_entry1.Equals(value))
+ {
+ RemoveAt(1);
+ return true;
+ }
+ else if (_count > 2)
+ {
+ if (_entry2.Equals(value))
+ {
+ RemoveAt(2);
+ return true;
+ }
+ else if (_count > 3)
+ {
+ if (_entry3.Equals(value))
+ {
+ RemoveAt(3);
+ return true;
+ }
+ else if (_count > 4)
+ {
+ if (_entry4.Equals(value))
+ {
+ RemoveAt(4);
+ return true;
+ }
+ else if (6 == _count && _entry5.Equals(value))
+ {
+ RemoveAt(5);
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public override void RemoveAt(int index)
+ {
+ // Remove entry at index, wipe out the last entry and move
+ // all the other entries up. Because we only have six
+ // entries we can just unravel all the cases.
+ switch (index)
+ {
+ case 0:
+ _entry0 = _entry1;
+ _entry1 = _entry2;
+ _entry2 = _entry3;
+ _entry3 = _entry4;
+ _entry4 = _entry5;
+ break;
+
+ case 1:
+ _entry1 = _entry2;
+ _entry2 = _entry3;
+ _entry3 = _entry4;
+ _entry4 = _entry5;
+ break;
+
+ case 2:
+ _entry2 = _entry3;
+ _entry3 = _entry4;
+ _entry4 = _entry5;
+ break;
+
+ case 3:
+ _entry3 = _entry4;
+ _entry4 = _entry5;
+ break;
+
+ case 4:
+ _entry4 = _entry5;
+ break;
+
+ case 5:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ _entry5 = default;
+ --_count;
+ }
+
+ public override T EntryAt(int index)
+ {
+ switch (index)
+ {
+ case 0:
+ return _entry0;
+
+ case 1:
+ return _entry1;
+
+ case 2:
+ return _entry2;
+
+ case 3:
+ return _entry3;
+
+ case 4:
+ return _entry4;
+
+ case 5:
+ return _entry5;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ public override void Promote(FrugalListBase oldList)
+ {
+ var oldCount = oldList.Count;
+ if (SIZE >= oldCount)
+ {
+ SetCount(oldList.Count);
+
+ switch (oldCount)
+ {
+ case 6:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ SetAt(4, oldList.EntryAt(4));
+ SetAt(5, oldList.EntryAt(5));
+ break;
+
+ case 5:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ SetAt(4, oldList.EntryAt(4));
+ break;
+
+ case 4:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ break;
+
+ case 3:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ break;
+
+ case 2:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ break;
+
+ case 1:
+ SetAt(0, oldList.EntryAt(0));
+ break;
+
+ case 0:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(oldList));
+ }
+ }
+ else
+ {
+ // this list is smaller than oldList
+ throw new ArgumentException($"Cannot promote from '{oldList}' to '{ToString()}' because the target map is too small.", nameof(oldList));
+ }
+ }
+
+ // Class specific implementation to avoid virtual method calls and additional logic
+ public void Promote(ThreeItemList oldList)
+ {
+ var oldCount = oldList.Count;
+
+ if (oldCount <= SIZE)
+ {
+ SetCount(oldList.Count);
+
+ switch (oldCount)
+ {
+ case 3:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ break;
+
+ case 2:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ break;
+
+ case 1:
+ SetAt(0, oldList.EntryAt(0));
+ break;
+
+ case 0:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(oldList));
+ }
+ }
+ else
+ {
+ // this list is smaller than oldList
+ throw new ArgumentException($"Cannot promote from '{oldList}' to '{ToString()}' because the target map is too small.", nameof(oldList));
+ }
+ }
+
+ // Class specific implementation to avoid virtual method calls and additional logic
+ public void Promote(SixItemList oldList)
+ {
+ var oldCount = oldList.Count;
+
+ SetCount(oldList.Count);
+
+ switch (oldCount)
+ {
+ case 6:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ SetAt(4, oldList.EntryAt(4));
+ SetAt(5, oldList.EntryAt(5));
+ break;
+
+ case 5:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ SetAt(4, oldList.EntryAt(4));
+ break;
+
+ case 4:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ break;
+
+ case 3:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ break;
+
+ case 2:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ break;
+
+ case 1:
+ SetAt(0, oldList.EntryAt(0));
+ break;
+
+ case 0:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(oldList));
+ }
+ }
+
+ public override T[] ToArray()
+ {
+ var array = new T[_count];
+
+ if (_count >= 1)
+ {
+ array[0] = _entry0;
+ if (_count >= 2)
+ {
+ array[1] = _entry1;
+ if (_count >= 3)
+ {
+ array[2] = _entry2;
+ if (_count >= 4)
+ {
+ array[3] = _entry3;
+ if (_count >= 5)
+ {
+ array[4] = _entry4;
+ if (_count == 6)
+ {
+ array[5] = _entry5;
+ }
+ }
+ }
+ }
+ }
+ }
+ return array;
+ }
+
+ public override void CopyTo(T[] array, int index)
+ {
+ if (_count >= 1)
+ {
+ array[index] = _entry0;
+ if (_count >= 2)
+ {
+ array[index + 1] = _entry1;
+ if (_count >= 3)
+ {
+ array[index + 2] = _entry2;
+ if (_count >= 4)
+ {
+ array[index + 3] = _entry3;
+ if (_count >= 5)
+ {
+ array[index + 4] = _entry4;
+ if (_count == 6)
+ {
+ array[index + 5] = _entry5;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public override object Clone()
+ {
+ var newList = new SixItemList();
+
+ newList.Promote(this);
+
+ return newList;
+ }
+
+ private void SetCount(int value)
+ {
+ if (value >= 0 && value <= SIZE)
+ {
+ _count = value;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ }
+ }
+
+ ///
+ /// A simple class to handle an array of 7 or more items. It is unsorted and uses
+ /// a linear search.
+ ///
+ internal sealed class ArrayItemList : FrugalListBase
+ {
+ // MINSIZE and GROWTH chosen to minimize memory footprint
+ private const int MINSIZE = 9;
+ private const int GROWTH = 3;
+ private const int LARGEGROWTH = 18;
+
+ private T[] _entries;
+
+ public ArrayItemList()
+ {
+ }
+
+ public ArrayItemList(int size)
+ {
+ // Make size a multiple of GROWTH
+ size += GROWTH - 1;
+
+ size -= size % GROWTH;
+
+ _entries = new T[size];
+ }
+
+ public ArrayItemList(ICollection collection)
+ {
+ if (collection == null)
+ {
+ return;
+ }
+
+ _count = collection.Count;
+
+ _entries = new T[_count];
+
+ collection.CopyTo(_entries, 0);
+ }
+
+ public ArrayItemList(ICollection collection)
+ {
+ if (collection == null)
+ {
+ return;
+ }
+
+ _count = collection.Count;
+
+ _entries = new T[_count];
+
+ collection.CopyTo(_entries, 0);
+ }
+
+ // Capacity of this store
+ public override int Capacity
+ {
+ get
+ {
+ return _entries?.Length ?? 0;
+ }
+ }
+
+ public override FrugalListStoreState Add(T value)
+ {
+ // If we don't have any entries or the existing entry is being overwritten,
+ // then we can use this store. Otherwise we have to promote.
+ if (null != _entries && _count < _entries.Length)
+ {
+ _entries[_count] = value;
+
+ ++_count;
+ }
+ else
+ {
+ if (null != _entries)
+ {
+ var size = _entries.Length;
+
+ // Grow the list slowly while it is small but
+ // faster once it reaches the LARGEGROWTH size
+ if (size < LARGEGROWTH)
+ {
+ size += GROWTH;
+ }
+ else
+ {
+ size += size >> 2;
+ }
+
+ var destEntries = new T[size];
+
+ // Copy old array
+ Array.Copy(_entries, 0, destEntries, 0, _entries.Length);
+
+ _entries = destEntries;
+ }
+ else
+ {
+ _entries = new T[MINSIZE];
+ }
+
+ // Insert into new array
+ _entries[_count] = value;
+
+ ++_count;
+ }
+
+ return FrugalListStoreState.Success;
+ }
+
+ public override void Clear()
+ {
+ // Wipe out the info.
+ for (var i = 0; i < _count; ++i)
+ {
+ _entries[i] = default;
+ }
+
+ _count = 0;
+ }
+
+ public override bool Contains(T value)
+ {
+ return -1 != IndexOf(value);
+ }
+
+ public override int IndexOf(T value)
+ {
+ for (var index = 0; index < _count; ++index)
+ {
+ if (_entries[index].Equals(value))
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ public override void Insert(int index, T value)
+ {
+ if (null != _entries && _count < _entries.Length)
+ {
+ // Move down the required number of items
+ Array.Copy(_entries, index, _entries, index + 1, _count - index);
+
+ // Put in the new item at the specified index
+ _entries[index] = value;
+
+ ++_count;
+
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public override void SetAt(int index, T value)
+ {
+ // Overwrite item at index
+ _entries[index] = value;
+ }
+
+ public override bool Remove(T value)
+ {
+ for (var index = 0; index < _count; ++index)
+ {
+ if (_entries[index].Equals(value))
+ {
+ RemoveAt(index);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public override void RemoveAt(int index)
+ {
+ // Shift entries down
+ var numToCopy = _count - index - 1;
+
+ if (numToCopy > 0)
+ {
+ Array.Copy(_entries, index + 1, _entries, index, numToCopy);
+ }
+
+ // Wipe out the last entry
+ _entries[_count - 1] = default;
+
+ --_count;
+ }
+
+ public override T EntryAt(int index)
+ {
+ return _entries[index];
+ }
+
+ public override void Promote(FrugalListBase oldList)
+ {
+ for (var index = 0; index < oldList.Count; ++index)
+ {
+ if (FrugalListStoreState.Success == Add(oldList.EntryAt(index)))
+ {
+ continue;
+ }
+
+ // this list is smaller than oldList
+ throw new ArgumentException($"Cannot promote from '{oldList}' to '{ToString()}' because the target map is too small.", nameof(oldList));
+ }
+ }
+
+ // Class specific implementation to avoid virtual method calls and additional logic
+ public void Promote(SixItemList oldList)
+ {
+ var oldCount = oldList.Count;
+
+ SetCount(oldList.Count);
+
+ switch (oldCount)
+ {
+ case 6:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ SetAt(4, oldList.EntryAt(4));
+ SetAt(5, oldList.EntryAt(5));
+ break;
+
+ case 5:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ SetAt(4, oldList.EntryAt(4));
+ break;
+
+ case 4:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ SetAt(3, oldList.EntryAt(3));
+ break;
+
+ case 3:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ SetAt(2, oldList.EntryAt(2));
+ break;
+
+ case 2:
+ SetAt(0, oldList.EntryAt(0));
+ SetAt(1, oldList.EntryAt(1));
+ break;
+
+ case 1:
+ SetAt(0, oldList.EntryAt(0));
+ break;
+
+ case 0:
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(oldList));
+ }
+ }
+
+ // Class specific implementation to avoid virtual method calls and additional logic
+ public void Promote(ArrayItemList oldList)
+ {
+ var oldCount = oldList.Count;
+
+ if (_entries.Length >= oldCount)
+ {
+ SetCount(oldList.Count);
+
+ for (var index = 0; index < oldCount; ++index)
+ {
+ SetAt(index, oldList.EntryAt(index));
+ }
+ }
+ else
+ {
+ // this list is smaller than oldList
+ throw new ArgumentException($"Cannot promote from '{oldList}' to '{ToString()}' because the target map is too small.", nameof(oldList));
+ }
+ }
+
+ public override T[] ToArray()
+ {
+ var array = new T[_count];
+
+ for (var i = 0; i < _count; ++i)
+ {
+ array[i] = _entries[i];
+ }
+
+ return array;
+ }
+
+ public override void CopyTo(T[] array, int index)
+ {
+ for (var i = 0; i < _count; ++i)
+ {
+ array[index + i] = _entries[i];
+ }
+ }
+
+ public override object Clone()
+ {
+ var newList = new ArrayItemList(Capacity);
+
+ newList.Promote(this);
+
+ return newList;
+ }
+
+ private void SetCount(int value)
+ {
+ if (value >= 0 && value <= _entries.Length)
+ {
+ _count = value;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ }
+
+ public override Compacter NewCompacter(int newCount)
+ {
+ return new ArrayCompacter(this, newCount);
+ }
+
+ // array-based implementation - compacts in-place or into a new array
+ internal class ArrayCompacter : Compacter
+ {
+ private readonly ArrayItemList _targetStore;
+ private readonly T[] _sourceArray;
+ private readonly T[] _targetArray;
+
+ public ArrayCompacter(ArrayItemList store, int newCount)
+ : base(store, newCount)
+ {
+ _sourceArray = store._entries;
+
+ // compute capacity for target array
+ // the first term agrees with AIL.Add, which grows by 5/4
+ var newCapacity = Math.Max(newCount + (newCount >> 2), MINSIZE);
+
+ if (newCapacity + (newCapacity >> 2) >= _sourceArray.Length)
+ {
+ // if there's not much space to be reclaimed, compact in place
+ _targetStore = store;
+ }
+ else
+ {
+ // otherwise, compact into a smaller array
+ _targetStore = new ArrayItemList(newCapacity);
+ }
+
+ _targetArray = _targetStore._entries;
+ }
+
+ protected override void IncludeOverride(int start, int end)
+ {
+ // bulk move
+ var size = end - start;
+ Array.Copy(_sourceArray, start, _targetArray, _validItemCount, size);
+ _validItemCount += size;
+
+ /* The following code is necessary in the general case, to avoid
+ * aliased entries in the old array. But the only user of Compacter
+ * is DependentList, where aliased entries are not a problem - they'll
+ * just get GC'd along with the old array.
+
+ // when not compacting in place, clear out entries in the source
+ if (_targetArray != _sourceArray)
+ {
+ T filler = default(T);
+ for (int i=_previousEnd; i Finish()
+ {
+ // clear out vacated entries in the source
+ var filler = default(T);
+ if (_sourceArray == _targetArray)
+ {
+ // in-place array source
+ for (int i = _validItemCount, n = _store.Count; i < n; ++i)
+ {
+ _sourceArray[i] = filler;
+ }
+ }
+ else
+ {
+ // array source to new array target
+ /* this code is not needed - see remarks in IncludeOverride()
+ for (int i=_previousEnd, n=_store._count; i
+ {
+ internal FrugalListBase _listStore;
+
+ public FrugalObjectList()
+ {
+ }
+
+ public FrugalObjectList(int size)
+ {
+ Capacity = size;
+ }
+
+ public int Capacity
+ {
+ get
+ {
+ return _listStore?.Capacity ?? 0;
+ }
+ set
+ {
+ var capacity = 0;
+
+ if (null != _listStore)
+ {
+ capacity = _listStore.Capacity;
+ }
+
+ if (capacity < value)
+ {
+ // Need to move to a more complex storage
+ FrugalListBase newStore;
+
+ if (value == 1)
+ {
+ newStore = new SingleItemList();
+ }
+ else if (value <= 3)
+ {
+ newStore = new ThreeItemList();
+ }
+ else if (value <= 6)
+ {
+ newStore = new SixItemList();
+ }
+ else
+ {
+ newStore = new ArrayItemList(value);
+ }
+
+ if (null != _listStore)
+ {
+ // Move entries in the old store to the new one
+ newStore.Promote(_listStore);
+ }
+
+ _listStore = newStore;
+ }
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ return _listStore?.Count ?? 0;
+ }
+ }
+
+
+ public T this[int index]
+ {
+ get
+ {
+ // If no entry, default(T) is returned
+ if (null != _listStore && index < _listStore.Count && index >= 0)
+ {
+ return _listStore.EntryAt(index);
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ set
+ {
+ // Ensure write success
+ if (null != _listStore && index < _listStore.Count && index >= 0)
+ {
+ _listStore.SetAt(index, value);
+
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ public int Add(T value)
+ {
+ if (null != _listStore)
+ {
+ // This is done because forward branches
+ // default prediction is not to be taken
+ // making this a CPU win because Add is
+ // a common operation.
+ }
+ else
+ {
+ _listStore = new SingleItemList();
+ }
+
+ var myState = _listStore.Add(value);
+
+ switch (myState) {
+ case FrugalListStoreState.Success:
+ break;
+ // Need to move to a more complex storage
+ // Allocate the store, promote, and add using the derived classes
+ // to avoid virtual method calls
+ case FrugalListStoreState.ThreeItemList:
+ {
+ var newStore = new ThreeItemList();
+
+ // Extract the values from the old store and insert them into the new store
+ newStore.Promote(_listStore);
+
+ // Insert the new item
+ newStore.Add(value);
+
+ _listStore = newStore;
+ break;
+ }
+ case FrugalListStoreState.SixItemList:
+ {
+ var newStore = new SixItemList();
+
+ // Extract the values from the old store and insert them into the new store
+ newStore.Promote(_listStore);
+
+ _listStore = newStore;
+
+ // Insert the new item
+ newStore.Add(value);
+
+ _listStore = newStore;
+ break;
+ }
+ case FrugalListStoreState.Array:
+ {
+ var newStore = new ArrayItemList(_listStore.Count + 1);
+
+ // Extract the values from the old store and insert them into the new store
+ newStore.Promote(_listStore);
+
+ _listStore = newStore;
+
+ // Insert the new item
+ newStore.Add(value);
+
+ _listStore = newStore;
+ break;
+ }
+ default:
+ throw new InvalidOperationException("Cannot promote from Array.");
+ }
+
+ return _listStore.Count - 1;
+ }
+
+ public void Clear()
+ {
+ _listStore?.Clear();
+ }
+
+ public bool Contains(T value)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.Contains(value);
+ }
+
+ return false;
+ }
+
+ public int IndexOf(T value)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.IndexOf(value);
+ }
+
+ return -1;
+ }
+
+ public void Insert(int index, T value)
+ {
+ if (index == 0 || _listStore != null && index <= _listStore.Count && index >= 0)
+ {
+ // Make sure we have a place to put the item
+ var minCapacity = 1;
+
+ if (null != _listStore && _listStore.Count == _listStore.Capacity)
+ {
+ // Store is full
+ minCapacity = Capacity + 1;
+ }
+
+ // Make the Capacity at *least* this big
+ Capacity = minCapacity;
+
+ _listStore?.Insert(index, value);
+
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public bool Remove(T value)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.Remove(value);
+ }
+
+ return false;
+ }
+
+ public void RemoveAt(int index)
+ {
+ if (null != _listStore && index < _listStore.Count && index >= 0)
+ {
+ _listStore.RemoveAt(index);
+
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public void EnsureIndex(int index)
+ {
+ if (index >= 0)
+ {
+ var delta = index + 1 - Count;
+ if (delta > 0)
+ {
+ // Grow the store
+ Capacity = index + 1;
+
+ var filler = default(T);
+
+ // Insert filler structs or objects
+ for (var i = 0; i < delta; ++i)
+ {
+ _listStore.Add(filler);
+ }
+ }
+
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public T[] ToArray()
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.ToArray();
+ }
+
+ return null;
+ }
+
+ public void CopyTo(T[] array, int index)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ _listStore.CopyTo(array, index);
+ }
+ }
+
+ public FrugalObjectList Clone()
+ {
+ var myClone = new FrugalObjectList();
+
+ if (null != _listStore)
+ {
+ myClone._listStore = (FrugalListBase)_listStore.Clone();
+ }
+
+ return myClone;
+ }
+
+ // helper class - compacts the valid entries, while removing the invalid ones.
+ // Usage:
+ // Compacter compacter = new Compacter(this, newCount);
+ // compacter.Include(start, end); // repeat as necessary
+ // compacter.Finish();
+ // newCount is the expected number of valid entries - used to help choose
+ // a target array of appropriate capacity
+ // Include(start, end) moves the entries in positions start, ..., end-1 toward
+ // the beginning, appending to the end of the "valid" area. Successive calls
+ // must be monotonic - i.e. the next 'start' must be >= the previous 'end'.
+ // Also, the sum of the block sizes (end-start) cannot exceed newCount.
+ // Finish() puts the provisional target array into permanent use.
+
+ protected class Compacter
+ {
+ private readonly FrugalObjectList _list;
+ private readonly FrugalListBase.Compacter _storeCompacter;
+
+ public Compacter(FrugalObjectList list, int newCount)
+ {
+ _list = list;
+
+ var store = _list._listStore;
+
+ _storeCompacter = store?.NewCompacter(newCount);
+ }
+
+ public void Include(int start, int end)
+ {
+ _storeCompacter.Include(start, end);
+ }
+
+ public void Finish()
+ {
+ if (_storeCompacter != null)
+ {
+ _list._listStore = _storeCompacter.Finish();
+ }
+ }
+ }
+ }
+
+ // Use FrugalStructList when only one reference to the list is needed.
+ // The "struct" in FrugalStructList refers to the list itself, not what the list contains.
+ internal struct FrugalStructList
+ {
+ internal FrugalListBase _listStore;
+
+ public FrugalStructList(int size)
+ {
+ _listStore = null;
+ Capacity = size;
+ }
+
+ public FrugalStructList(ICollection collection)
+ {
+ if (collection.Count > 6)
+ {
+ _listStore = new ArrayItemList(collection);
+ }
+ else
+ {
+ _listStore = null;
+
+ Capacity = collection.Count;
+
+ foreach (T item in collection)
+ {
+ Add(item);
+ }
+ }
+ }
+
+ public FrugalStructList(ICollection collection)
+ {
+ if (collection.Count > 6)
+ {
+ _listStore = new ArrayItemList(collection);
+ }
+ else
+ {
+ _listStore = null;
+
+ Capacity = collection.Count;
+
+ foreach (var item in collection)
+ {
+ Add(item);
+ }
+ }
+ }
+
+ public int Capacity
+ {
+ get
+ {
+ if (null != _listStore)
+ {
+ return _listStore.Capacity;
+ }
+
+ return 0;
+ }
+ set
+ {
+ var capacity = 0;
+
+ if (null != _listStore)
+ {
+ capacity = _listStore.Capacity;
+ }
+
+ if (capacity < value)
+ {
+ // Need to move to a more complex storage
+ FrugalListBase newStore;
+
+ if (value == 1)
+ {
+ newStore = new SingleItemList();
+ }
+ else if (value <= 3)
+ {
+ newStore = new ThreeItemList();
+ }
+ else if (value <= 6)
+ {
+ newStore = new SixItemList();
+ }
+ else
+ {
+ newStore = new ArrayItemList(value);
+ }
+
+ if (null != _listStore)
+ {
+ // Move entries in the old store to the new one
+ newStore.Promote(_listStore);
+ }
+
+ _listStore = newStore;
+ }
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ return _listStore?.Count ?? 0;
+ }
+ }
+
+
+ public T this[int index]
+ {
+ get
+ {
+ // If no entry, default(T) is returned
+ if (null != _listStore && index < _listStore.Count && index >= 0)
+ {
+ return _listStore.EntryAt(index);
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ set
+ {
+ // Ensure write success
+ if (null != _listStore && index < _listStore.Count && index >= 0)
+ {
+ _listStore.SetAt(index, value);
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+ }
+
+ public int Add(T value)
+ {
+ if (null != _listStore)
+ {
+ // This is done because forward branches
+ // default prediction is not to be taken
+ // making this a CPU win because Add is
+ // a common operation.
+ }
+ else
+ {
+ _listStore = new SingleItemList();
+ }
+
+ var myState = _listStore.Add(value);
+ switch (myState) {
+ case FrugalListStoreState.Success:
+ break;
+ // Need to move to a more complex storage
+ // Allocate the store, promote, and add using the derived classes
+ // to avoid virtual method calls
+ case FrugalListStoreState.ThreeItemList:
+ {
+ var newStore = new ThreeItemList();
+
+ // Extract the values from the old store and insert them into the new store
+ newStore.Promote(_listStore);
+
+ // Insert the new item
+ newStore.Add(value);
+ _listStore = newStore;
+ break;
+ }
+ case FrugalListStoreState.SixItemList:
+ {
+ var newStore = new SixItemList();
+
+ // Extract the values from the old store and insert them into the new store
+ newStore.Promote(_listStore);
+ _listStore = newStore;
+
+ // Insert the new item
+ newStore.Add(value);
+ _listStore = newStore;
+ break;
+ }
+ case FrugalListStoreState.Array:
+ {
+ var newStore = new ArrayItemList(_listStore.Count + 1);
+
+ // Extract the values from the old store and insert them into the new store
+ newStore.Promote(_listStore);
+ _listStore = newStore;
+
+ // Insert the new item
+ newStore.Add(value);
+ _listStore = newStore;
+ break;
+ }
+ default:
+ throw new InvalidOperationException("Cannot promote from Array.");
+ }
+
+ return _listStore.Count - 1;
+ }
+
+ public void Clear()
+ {
+ _listStore?.Clear();
+ }
+
+ public bool Contains(T value)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.Contains(value);
+ }
+
+ return false;
+ }
+
+ public int IndexOf(T value)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.IndexOf(value);
+ }
+
+ return -1;
+ }
+
+ public void Insert(int index, T value)
+ {
+ if (index == 0 || null != _listStore && index <= _listStore.Count && index >= 0)
+ {
+ // Make sure we have a place to put the item
+ var minCapacity = 1;
+
+ if (null != _listStore && _listStore.Count == _listStore.Capacity)
+ {
+ // Store is full
+ minCapacity = Capacity + 1;
+ }
+
+ // Make the Capacity at *least* this big
+ Capacity = minCapacity;
+
+ _listStore.Insert(index, value);
+
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public bool Remove(T value)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.Remove(value);
+ }
+
+ return false;
+ }
+
+ public void RemoveAt(int index)
+ {
+ if (null != _listStore && index < _listStore.Count && index >= 0)
+ {
+ _listStore.RemoveAt(index);
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public void EnsureIndex(int index)
+ {
+ if (index >= 0)
+ {
+ var delta = index + 1 - Count;
+ if (delta > 0)
+ {
+ // Grow the store
+ Capacity = index + 1;
+
+ var filler = default(T);
+
+ // Insert filler structs or objects
+ for (var i = 0; i < delta; ++i)
+ {
+ _listStore.Add(filler);
+ }
+ }
+ return;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ public T[] ToArray()
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ return _listStore.ToArray();
+ }
+
+ return null;
+ }
+
+ public void CopyTo(T[] array, int index)
+ {
+ if (null != _listStore && _listStore.Count > 0)
+ {
+ _listStore.CopyTo(array, index);
+ }
+ }
+
+ public FrugalStructList Clone()
+ {
+ var myClone = new FrugalStructList();
+
+ if (null != _listStore)
+ {
+ myClone._listStore = (FrugalListBase)_listStore.Clone();
+ }
+
+ return myClone;
+ }
+ }
+}
+
diff --git a/src/Avalonia.Visuals/Utilities/MappedArraySlice.cs b/src/Avalonia.Visuals/Utilities/MappedArraySlice.cs
new file mode 100644
index 00000000000..299c6077313
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/MappedArraySlice.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+ ///
+ /// Provides a mapped view of an underlying slice, selecting arbitrary indices
+ /// from the source array.
+ ///
+ /// The type of item contained in the underlying array.
+ internal readonly struct MappedArraySlice
+ where T : struct
+ {
+ private readonly ArraySlice _data;
+ private readonly ArraySlice _map;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The data slice.
+ /// The map slice.
+ public MappedArraySlice(in ArraySlice data, in ArraySlice map)
+ {
+#if DEBUG
+ if (map.Length.CompareTo(data.Length) > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(map));
+ }
+#endif
+
+ _data = data;
+ _map = map;
+ }
+
+ ///
+ /// Gets the number of items in the map.
+ ///
+ public int Length => _map.Length;
+
+ ///
+ /// Returns a reference to specified element of the slice.
+ ///
+ /// The index of the element to return.
+ /// The .
+ ///
+ /// Thrown when index less than 0 or index greater than or equal to .
+ ///
+ public ref T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref _data[_map[index]];
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs
index 5feaa88e26c..ee85d1e8761 100644
--- a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs
+++ b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
@@ -10,15 +11,37 @@ namespace Avalonia.Utilities
///
/// The type of elements in the slice.
[DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
- public readonly struct ReadOnlySlice : IReadOnlyList
+ public readonly struct ReadOnlySlice : IReadOnlyList where T : struct
{
+ private readonly int _offset;
+
+ ///
+ /// Gets an empty
+ ///
+ public static ReadOnlySlice Empty => new ReadOnlySlice(Array.Empty());
+
+ private readonly ReadOnlyMemory _buffer;
+
public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { }
- public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length)
+ public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int offset = 0)
{
- Buffer = buffer;
+#if DEBUG
+ if (start.CompareTo(0) < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof (start));
+ }
+
+ if (length.CompareTo(buffer.Length) > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof (length));
+ }
+#endif
+
+ _buffer = buffer;
Start = start;
Length = length;
+ _offset = offset;
}
///
@@ -51,12 +74,38 @@ public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length)
public bool IsEmpty => Length == 0;
///
- /// The buffer.
+ /// The underlying span.
///
- public ReadOnlyMemory Buffer { get; }
+ public ReadOnlySpan Span => _buffer.Span.Slice(_offset, Length);
- public T this[int index] => Buffer.Span[index];
+ ///
+ /// The underlying buffer.
+ ///
+ public ReadOnlyMemory Buffer => _buffer;
+ ///
+ /// Returns a value to specified element of the slice.
+ ///
+ /// The index of the element to return.
+ /// The .
+ ///
+ /// Thrown when index less than 0 or index greater than or equal to .
+ ///
+ public T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+#if DEBUG
+ if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof (index));
+ }
+#endif
+ return Span[index];
+ }
+ }
+
///
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
///
@@ -70,19 +119,22 @@ public ReadOnlySlice AsSlice(int start, int length)
return this;
}
- if (start < Start || start > End)
+ if (length == 0)
+ {
+ return Empty;
+ }
+
+ if (start < 0 || _offset + start > _buffer.Length - 1)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
- if (start + length > Start + Length)
+ if (_offset + start + length > _buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
- var bufferOffset = start - Start;
-
- return new ReadOnlySlice(Buffer.Slice(bufferOffset), start, length);
+ return new ReadOnlySlice(_buffer, start, length, _offset);
}
///
@@ -102,7 +154,7 @@ public ReadOnlySlice Take(int length)
throw new ArgumentOutOfRangeException(nameof(length));
}
- return new ReadOnlySlice(Buffer.Slice(0, length), Start, length);
+ return new ReadOnlySlice(_buffer, Start, length, _offset);
}
///
@@ -122,7 +174,7 @@ public ReadOnlySlice Skip(int length)
throw new ArgumentOutOfRangeException(nameof(length));
}
- return new ReadOnlySlice(Buffer.Slice(length), Start + length, Length - length);
+ return new ReadOnlySlice(_buffer, Start + length, Length - length, _offset + length);
}
///
@@ -174,7 +226,7 @@ public ReadOnlySliceDebugView(ReadOnlySlice readOnlySlice)
public bool IsEmpty => _readOnlySlice.IsEmpty;
- public ReadOnlyMemory Items => _readOnlySlice.Buffer;
+ public ReadOnlySpan Items => _readOnlySlice.Span;
}
}
}
diff --git a/src/Avalonia.Visuals/Utilities/Span.cs b/src/Avalonia.Visuals/Utilities/Span.cs
new file mode 100644
index 00000000000..7eb9652d9f0
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/Span.cs
@@ -0,0 +1,596 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+//+-----------------------------------------------------------------------
+//
+//
+//
+// Contents: Generic span types
+//
+// [As of this creation, C# has no real generic type system]
+//
+
+using System;
+using System.Collections;
+using System.Diagnostics;
+
+namespace Avalonia.Utilities
+{
+ internal class Span
+ {
+ public readonly object? element;
+ public int length;
+
+ public Span(object? element, int length)
+ {
+ this.element = element;
+ this.length = length;
+ }
+ }
+
+ ///
+ /// VECTOR: A series of spans
+ ///
+ internal class SpanVector : IEnumerable
+ {
+ private static readonly Equals s_referenceEquals = object.ReferenceEquals;
+ private static readonly Equals s_equals = object.Equals;
+
+ private FrugalStructList _spans;
+
+ internal SpanVector(
+ object? defaultObject,
+ FrugalStructList spans = new FrugalStructList())
+ {
+ Default = defaultObject;
+ _spans = spans;
+ }
+
+
+ ///
+ /// Get enumerator to vector
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return new SpanEnumerator(this);
+ }
+
+ ///
+ /// Add a new span to vector
+ ///
+ private void Add(Span span)
+ {
+ _spans.Add(span);
+ }
+
+ ///
+ /// Delete n elements of vector
+ ///
+ internal virtual void Delete(int index, int count, ref SpanPosition latestPosition)
+ {
+ DeleteInternal(index, count);
+
+ if (index <= latestPosition.Index)
+ latestPosition = new SpanPosition();
+ }
+
+ private void DeleteInternal(int index, int count)
+ {
+ // Do removes highest index to lowest to minimize the number
+ // of array entries copied.
+ for (var i = index + count - 1; i >= index; --i)
+ {
+ _spans.RemoveAt(i);
+ }
+ }
+
+ ///
+ /// Insert n elements to vector
+ ///
+ private void Insert(int index, int count)
+ {
+ for (var c = 0; c < count; c++)
+ _spans.Insert(index, new Span(null, 0));
+ }
+
+ ///
+ /// Finds the span that contains the specified character position.
+ ///
+ /// position to find
+ /// Position of the most recently accessed span (e.g., the current span
+ /// of a SpanRider) for performance; FindSpan runs in O(1) time if the specified cp is in the same span
+ /// or an adjacent span.
+ /// receives the index and first cp of the span that contains the specified
+ /// position or, if the position is past the end of the vector, the index and cp just past the end of
+ /// the last span.
+ /// Returns true if cp is in range or false if not.
+ internal bool FindSpan(int cp, SpanPosition latestPosition, out SpanPosition spanPosition)
+ {
+ Debug.Assert(cp >= 0);
+
+ var spanCount = _spans.Count;
+ int spanIndex, spanCP;
+
+ if (cp == 0)
+ {
+ // CP zero always corresponds to span index zero
+ spanIndex = 0;
+ spanCP = 0;
+ }
+ else if (cp >= latestPosition.Offset || cp * 2 < latestPosition.Offset)
+ {
+ // One of the following is true:
+ // 1. cp is after the latest position (the most recently accessed span)
+ // 2. cp is closer to zero than to the latest position
+ if (cp >= latestPosition.Offset)
+ {
+ // case 1: scan forward from the latest position
+ spanIndex = latestPosition.Index;
+ spanCP = latestPosition.Offset;
+ }
+ else
+ {
+ // case 2: scan forward from the start of the span vector
+ spanIndex = 0;
+ spanCP = 0;
+ }
+
+ // Scan forward until we find the Span that contains the specified CP or
+ // reach the end of the SpanVector
+ for (; spanIndex < spanCount; ++spanIndex)
+ {
+ var spanLength = _spans[spanIndex].length;
+
+ if (cp < spanCP + spanLength)
+ {
+ break;
+ }
+
+ spanCP += spanLength;
+ }
+ }
+ else
+ {
+ // The specified CP is before the latest position but closer to it than to zero;
+ // therefore scan backwards from the latest position
+ spanIndex = latestPosition.Index;
+ spanCP = latestPosition.Offset;
+
+ while (spanCP > cp)
+ {
+ Debug.Assert(spanIndex > 0);
+ spanCP -= _spans[--spanIndex].length;
+ }
+ }
+
+ // Return index and cp of span in out param.
+ spanPosition = new SpanPosition(spanIndex, spanCP);
+
+ // Return true if the span is in range.
+ return spanIndex != spanCount;
+ }
+
+
+ ///
+ /// Set an element as a value to a character range
+ ///
+ ///
+ /// Implementation of span element object must implement Object.Equals to
+ /// avoid runtime reflection cost on equality check of nested-type object.
+ ///
+ public void SetValue(int first, int length, object element)
+ {
+ Set(first, length, element, SpanVector.s_equals, new SpanPosition());
+ }
+
+ ///
+ /// Set an element as a value to a character range; takes a SpanPosition of a recently accessed
+ /// span for performance and returns a known valid SpanPosition
+ ///
+ public SpanPosition SetValue(int first, int length, object element, SpanPosition spanPosition)
+ {
+ return Set(first, length, element, SpanVector.s_equals, spanPosition);
+ }
+
+ ///
+ /// Set an element as a reference to a character range
+ ///
+ public void SetReference(int first, int length, object element)
+ {
+ Set(first, length, element, SpanVector.s_referenceEquals, new SpanPosition());
+ }
+
+ ///
+ /// Set an element as a reference to a character range; takes a SpanPosition of a recently accessed
+ /// span for performance and returns a known valid SpanPosition
+ ///
+ public SpanPosition SetReference(int first, int length, object element, SpanPosition spanPosition)
+ {
+ return Set(first, length, element, SpanVector.s_referenceEquals, spanPosition);
+ }
+
+ private SpanPosition Set(int first, int length, object? element, Equals equals, SpanPosition spanPosition)
+ {
+ var inRange = FindSpan(first, spanPosition, out spanPosition);
+
+ // fs = index of first span partly or completely updated
+ // fc = character index at start of fs
+ var fs = spanPosition.Index;
+ var fc = spanPosition.Offset;
+
+ // Find the span that contains the first affected cp
+ if (!inRange)
+ {
+ // The first cp is past the end of the last span
+ if (fc < first)
+ {
+ // Create default run up to first
+ Add(new Span(Default, first - fc));
+ }
+
+ if (Count > 0
+ && equals(_spans[Count - 1].element, element))
+ {
+ // New Element matches end Element, just extend end Element
+ _spans[Count - 1].length += length;
+
+ // Make sure fs and fc still agree
+ if (fs == Count)
+ {
+ fc += length;
+ }
+ }
+ else
+ {
+ Add(new Span(element, length));
+ }
+ }
+ else
+ {
+ // Now find the last span affected by the update
+
+ var ls = fs;
+ var lc = fc;
+ while (ls < Count
+ && lc + _spans[ls].length <= first + length)
+ {
+ lc += _spans[ls].length;
+ ls++;
+ }
+ // ls = first span following update to remain unchanged in part or in whole
+ // lc = character index at start of ls
+
+ // expand update region backwards to include existing Spans of identical
+ // Element type
+
+ if (first == fc)
+ {
+ // Item at [fs] is completely replaced. Check prior item
+
+ if (fs > 0
+ && equals(_spans[fs - 1].element, element))
+ {
+ // Expand update area over previous run of equal classification
+ fs--;
+ fc -= _spans[fs].length;
+ first = fc;
+ length += _spans[fs].length;
+ }
+ }
+ else
+ {
+ // Item at [fs] is partially replaced. Check if it is same as update
+ if (equals(_spans[fs].element, element))
+ {
+ // Expand update area back to start of first affected equal valued run
+ length = first + length - fc;
+ first = fc;
+ }
+ }
+
+ // Expand update region forwards to include existing Spans of identical
+ // Element type
+
+ if (ls < Count
+ && equals(_spans[ls].element, element))
+ {
+ // Extend update region to end of existing split run
+
+ length = lc + _spans[ls].length - first;
+ lc += _spans[ls].length;
+ ls++;
+ }
+
+ // If no old Spans remain beyond area affected by update, handle easily:
+
+ if (ls >= Count)
+ {
+ // None of the old span list extended beyond the update region
+
+ if (fc < first)
+ {
+ // Updated region leaves some of [fs]
+
+ if (Count != fs + 2)
+ {
+ if (!Resize(fs + 2))
+ throw new OutOfMemoryException();
+ }
+ _spans[fs].length = first - fc;
+ _spans[fs + 1] = new Span(element, length);
+ }
+ else
+ {
+ // Updated item replaces [fs]
+ if (Count != fs + 1)
+ {
+ if (!Resize(fs + 1))
+ throw new OutOfMemoryException();
+ }
+ _spans[fs] = new Span(element, length);
+ }
+ }
+ else
+ {
+ // Record partial element type at end, if any
+
+ object? trailingElement = null;
+ var trailingLength = 0;
+
+ if (first + length > lc)
+ {
+ trailingElement = _spans[ls].element;
+ trailingLength = lc + _spans[ls].length - (first + length);
+ }
+
+ // Calculate change in number of Spans
+
+ var spanDelta = 1 // The new span
+ + (first > fc ? 1 : 0) // part span at start
+ - (ls - fs); // existing affected span count
+
+ // Note part span at end doesn't affect the calculation - the run may need
+ // updating, but it doesn't need creating.
+
+ if (spanDelta < 0)
+ {
+ DeleteInternal(fs + 1, -spanDelta);
+ }
+ else if (spanDelta > 0)
+ {
+ Insert(fs + 1, spanDelta);
+ // Initialize inserted Spans
+ for (var i = 0; i < spanDelta; i++)
+ {
+ _spans[fs + 1 + i] = new Span(null, 0);
+ }
+ }
+
+ // Assign Element values
+
+ // Correct Length of split span before updated range
+
+ if (fc < first)
+ {
+ _spans[fs].length = first - fc;
+ fs++;
+ fc = first;
+ }
+
+ // Record Element type for updated range
+
+ _spans[fs] = new Span(element, length);
+ fs++;
+ fc += length;
+
+ // Correct Length of split span following updated range
+
+ if (lc < first + length)
+ {
+ _spans[fs] = new Span(trailingElement, trailingLength);
+ }
+ }
+ }
+
+ // Return a known valid span position.
+ return new SpanPosition(fs, fc);
+ }
+
+ ///
+ /// Number of spans in vector
+ ///
+ public int Count
+ {
+ get { return _spans.Count; }
+ }
+
+ ///
+ /// The default element of vector
+ ///
+ public object? Default { get; }
+
+ ///
+ /// Span accessor at nth element
+ ///
+ public Span this[int index]
+ {
+ get { return _spans[index]; }
+ }
+
+ private bool Resize(int targetCount)
+ {
+ if (targetCount > Count)
+ {
+ for (var c = 0; c < targetCount - Count; c++)
+ {
+ _spans.Add(new Span(null, 0));
+ }
+ }
+ else if (targetCount < Count)
+ {
+ DeleteInternal(targetCount, Count - targetCount);
+ }
+ return true;
+ }
+ }
+
+ ///
+ /// Equality check method
+ ///
+ internal delegate bool Equals(object? first, object? second);
+
+ ///
+ /// ENUMERATOR: To navigate a vector through its element
+ ///
+ internal sealed class SpanEnumerator : IEnumerator
+ {
+ private readonly SpanVector _spans;
+ private int _current; // current span
+
+ internal SpanEnumerator(SpanVector spans)
+ {
+ _spans = spans;
+ _current = -1;
+ }
+
+ ///
+ /// The current span
+ ///
+ public object Current
+ {
+ get { return _spans[_current]; }
+ }
+
+ ///
+ /// Move to the next span
+ ///
+ public bool MoveNext()
+ {
+ _current++;
+
+ return _current < _spans.Count;
+ }
+
+ ///
+ /// Reset the enumerator
+ ///
+ public void Reset()
+ {
+ _current = -1;
+ }
+ }
+
+
+ ///
+ /// Represents a Span's position as a pair of related values: its index in the
+ /// SpanVector its CP offset from the start of the SpanVector.
+ ///
+ internal readonly struct SpanPosition
+ {
+ internal SpanPosition(int spanIndex, int spanOffset)
+ {
+ Index = spanIndex;
+ Offset = spanOffset;
+ }
+
+ internal int Index { get; }
+
+ internal int Offset { get; }
+ }
+
+ ///
+ /// RIDER: To navigate a vector through character index
+ ///
+ internal struct SpanRider
+ {
+ private readonly SpanVector _spans; // vector of spans
+ private SpanPosition _spanPosition; // index and cp of current span
+
+ public SpanRider(SpanVector spans, SpanPosition latestPosition) : this(spans, latestPosition, latestPosition.Offset)
+ {
+ }
+
+ public SpanRider(SpanVector spans, SpanPosition latestPosition = new SpanPosition(), int cp = 0)
+ {
+ _spans = spans;
+ _spanPosition = new SpanPosition();
+ CurrentPosition = 0;
+ Length = 0;
+ At(latestPosition, cp);
+ }
+
+ ///
+ /// Move rider to a given cp
+ ///
+ public bool At(int cp)
+ {
+ return At(_spanPosition, cp);
+ }
+
+ public bool At(SpanPosition latestPosition, int cp)
+ {
+ var inRange = _spans.FindSpan(cp, latestPosition, out _spanPosition);
+ if (inRange)
+ {
+ // cp is in range:
+ // - Length is the distance to the end of the span
+ // - CurrentPosition is cp
+ Length = _spans[_spanPosition.Index].length - (cp - _spanPosition.Offset);
+ CurrentPosition = cp;
+ }
+ else
+ {
+ // cp is out of range:
+ // - Length is the default span length
+ // - CurrentPosition is the end of the last span
+ Length = int.MaxValue;
+ CurrentPosition = _spanPosition.Offset;
+ }
+
+ return inRange;
+ }
+
+ ///
+ /// The first cp of the current span
+ ///
+ public int CurrentSpanStart
+ {
+ get { return _spanPosition.Offset; }
+ }
+
+ ///
+ /// The length of current span start from the current cp
+ ///
+ public int Length { get; private set; }
+
+ ///
+ /// The current position
+ ///
+ public int CurrentPosition { get; private set; }
+
+ ///
+ /// The element of the current span
+ ///
+ public object? CurrentElement
+ {
+ get { return _spanPosition.Index >= _spans.Count ? _spans.Default : _spans[_spanPosition.Index].element; }
+ }
+
+ ///
+ /// Index of the span at the current position.
+ ///
+ public int CurrentSpanIndex
+ {
+ get { return _spanPosition.Index; }
+ }
+
+ ///
+ /// Index and first cp of the current span.
+ ///
+ public SpanPosition SpanPosition
+ {
+ get { return _spanPosition; }
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 2694a61353a..e695a9cb410 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -441,17 +441,7 @@ public void DrawEllipse(IBrush brush, IPen pen, Rect rect)
}
}
}
-
- ///
- public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
- {
- using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
- {
- var textImpl = (FormattedTextImpl) text;
- textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
- }
- }
-
+
///
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
deleted file mode 100644
index 625d1c6f520..00000000000
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ /dev/null
@@ -1,838 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Utilities;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
- ///
- /// Skia formatted text implementation.
- ///
- internal class FormattedTextImpl : IFormattedTextImpl
- {
- private static readonly ThreadLocal t_builder = new ThreadLocal(() => new SKTextBlobBuilder());
-
- private const float MAX_LINE_WIDTH = 10000;
-
- private readonly List> _foregroundBrushes =
- new List>();
- private readonly List _lines = new List();
- private readonly SKPaint _paint;
- private readonly List _rects = new List();
- public string Text { get; }
- private readonly TextWrapping _wrapping;
- private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
- private float _lineHeight = 0;
- private float _lineOffset = 0;
- private Rect _bounds;
- private List _skiaLines;
- private ReadOnlySlice _glyphs;
- private ReadOnlySlice _advances;
-
- public FormattedTextImpl(
- string text,
- Typeface typeface,
- double fontSize,
- TextAlignment textAlignment,
- TextWrapping wrapping,
- Size constraint,
- IReadOnlyList spans)
- {
- Text = text ?? string.Empty;
-
- UpdateGlyphInfo(Text, typeface.GlyphTypeface, (float)fontSize);
-
- _paint = new SKPaint
- {
- TextEncoding = SKTextEncoding.Utf16,
- IsStroke = false,
- IsAntialias = true,
- LcdRenderText = true,
- SubpixelText = true,
- IsLinearText = true,
- Typeface = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).Typeface,
- TextSize = (float)fontSize,
- TextAlign = textAlignment.ToSKTextAlign()
- };
-
- //currently Skia does not measure properly with Utf8 !!!
- //Paint.TextEncoding = SKTextEncoding.Utf8;
-
- _wrapping = wrapping;
- _constraint = constraint;
-
- if (spans != null)
- {
- foreach (var span in spans)
- {
- if (span.ForegroundBrush != null)
- {
- SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
- }
- }
- }
-
- Rebuild();
- }
-
- public Size Constraint => _constraint;
-
- public Rect Bounds => _bounds;
-
- public IEnumerable GetLines()
- {
- return _lines;
- }
-
- public TextHitTestResult HitTestPoint(Point point)
- {
- float y = (float)point.Y;
-
- AvaloniaFormattedTextLine line = default;
-
- float nextTop = 0;
-
- foreach(var currentLine in _skiaLines)
- {
- if(currentLine.Top <= y)
- {
- line = currentLine;
- nextTop = currentLine.Top + currentLine.Height;
- }
- else
- {
- nextTop = currentLine.Top;
- break;
- }
- }
-
- if (!line.Equals(default(AvaloniaFormattedTextLine)))
- {
- var rects = GetRects();
-
- for (int c = line.Start; c < line.Start + line.TextLength; c++)
- {
- var rc = rects[c];
- if (rc.Contains(point))
- {
- return new TextHitTestResult
- {
- IsInside = !(line.TextLength > line.Length),
- TextPosition = c,
- IsTrailing = (point.X - rc.X) > rc.Width / 2
- };
- }
- }
-
- int offset = 0;
-
- if (point.X >= (rects[line.Start].X + line.Width) && line.Length > 0)
- {
- offset = line.TextLength > line.Length ?
- line.Length : (line.Length - 1);
- }
-
- if (y < nextTop)
- {
- return new TextHitTestResult
- {
- IsInside = false,
- TextPosition = line.Start + offset,
- IsTrailing = Text.Length == (line.Start + offset + 1)
- };
- }
- }
-
- bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height);
-
- return new TextHitTestResult()
- {
- IsInside = false,
- IsTrailing = end,
- TextPosition = end ? Text.Length - 1 : 0
- };
- }
-
- public Rect HitTestTextPosition(int index)
- {
- if (string.IsNullOrEmpty(Text))
- {
- var alignmentOffset = TransformX(0, 0, _paint.TextAlign);
- return new Rect(alignmentOffset, 0, 0, _lineHeight);
- }
- var rects = GetRects();
- if (index >= Text.Length || index < 0)
- {
- var r = rects.LastOrDefault();
-
- var c = Text[Text.Length - 1];
-
- switch (c)
- {
- case '\n':
- case '\r':
- return new Rect(r.X, r.Y, 0, _lineHeight);
- default:
- return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
- }
- }
- return rects[index];
- }
-
- public IEnumerable HitTestTextRange(int index, int length)
- {
- List result = new List();
-
- var rects = GetRects();
-
- int lastIndex = index + length - 1;
-
- foreach (var line in _skiaLines.Where(l =>
- (l.Start + l.Length) > index &&
- lastIndex >= l.Start &&
- !l.IsEmptyTrailingLine))
- {
- int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0);
-
- double left = rects[line.Start > index ? line.Start : index].X;
- double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right;
-
- result.Add(new Rect(left, line.Top, right - left, line.Height));
- }
-
- return result;
- }
-
- public override string ToString()
- {
- return Text;
- }
-
- private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint)
- {
- if(length == 0)
- {
- return;
- }
-
- var glyphs = _glyphs.Buffer.Span.Slice(start, length);
- var advances = _advances.Buffer.Span.Slice(start, length);
- var builder = t_builder.Value;
-
- var buffer = builder.AllocateHorizontalRun(_paint.ToFont(), length, 0);
-
- buffer.SetGlyphs(glyphs);
-
- var positions = buffer.GetPositionSpan();
-
- var pos = 0f;
-
- for (int i = 0; i < advances.Length; i++)
- {
- positions[i] = pos;
-
- pos += advances[i];
- }
-
- var blob = builder.Build();
-
- if(blob != null)
- {
- canvas.DrawText(blob, x, y, paint);
- }
- }
-
- internal void Draw(DrawingContextImpl context,
- SKCanvas canvas,
- SKPoint origin,
- DrawingContextImpl.PaintWrapper foreground,
- bool canUseLcdRendering)
- {
- /* TODO: This originated from Native code, it might be useful for debugging character positions as
- * we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
- * not needed anymore.
-
- SkPaint dpaint;
- ctx->Canvas->save();
- ctx->Canvas->translate(origin.fX, origin.fY);
- for (int c = 0; c < Lines.size(); c++)
- {
- dpaint.setARGB(255, 0, 0, 0);
- SkRect rc;
- rc.fLeft = 0;
- rc.fTop = Lines[c].Top;
- rc.fRight = Lines[c].Width;
- rc.fBottom = rc.fTop + LineOffset;
- ctx->Canvas->drawRect(rc, dpaint);
- }
- for (int c = 0; c < Length; c++)
- {
- dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10);
- dpaint.setStyle(SkPaint::kFill_Style);
- ctx->Canvas->drawRect(Rects[c], dpaint);
- }
- ctx->Canvas->restore();
- */
- using (var paint = _paint.Clone())
- {
- IDisposable currd = null;
- var currentWrapper = foreground;
- SKPaint currentPaint = null;
- try
- {
- ApplyWrapperTo(ref currentPaint, foreground, ref currd, paint, canUseLcdRendering);
- bool hasCusomFGBrushes = _foregroundBrushes.Any();
-
- for (int c = 0; c < _skiaLines.Count; c++)
- {
- AvaloniaFormattedTextLine line = _skiaLines[c];
-
- float x = TransformX(origin.X, line.Width, paint.TextAlign);
-
- if (!hasCusomFGBrushes)
- {
- DrawTextBlob(line.Start, line.Length, x, origin.Y + line.Top + _lineOffset, canvas, paint);
- }
- else
- {
- float currX = x;
- float measure;
- int len;
- float factor;
-
- switch (paint.TextAlign)
- {
- case SKTextAlign.Left:
- factor = 0;
- break;
- case SKTextAlign.Center:
- factor = 0.5f;
- break;
- case SKTextAlign.Right:
- factor = 1;
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
-
- currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor;
-
- for (int i = line.Start; i < line.Start + line.Length;)
- {
- var fb = GetNextForegroundBrush(ref line, i, out len);
-
- if (fb != null)
- {
- //TODO: figure out how to get the brush size
- currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
- new Size());
- }
- else
- {
- if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
- currentWrapper = foreground;
- }
-
- measure = MeasureText(i, len);
- currX += measure * factor;
-
- ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering);
-
- DrawTextBlob(i, len, currX, origin.Y + line.Top + _lineOffset, canvas, paint);
-
- i += len;
- currX += measure * (1 - factor);
- }
- }
- }
- }
- finally
- {
- if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
- currd?.Dispose();
- }
- }
- }
-
- private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper,
- ref IDisposable curr, SKPaint paint, bool canUseLcdRendering)
- {
- if (current == wrapper.Paint)
- return;
- curr?.Dispose();
- curr = wrapper.ApplyTo(paint);
- paint.LcdRenderText = canUseLcdRendering;
- }
-
- private static bool IsBreakChar(char c)
- {
- //white space or zero space whitespace
- return char.IsWhiteSpace(c) || c == '\u200B';
- }
-
- private static int LineBreak(string textInput, int textIndex, int stop,
- SKPaint paint, float maxWidth,
- out int trailingCount)
- {
- int lengthBreak;
- if (maxWidth == -1)
- {
- lengthBreak = stop - textIndex;
- }
- else
- {
- string subText = textInput.Substring(textIndex, stop - textIndex);
- lengthBreak = (int)paint.BreakText(subText, maxWidth, out _);
- }
-
- //Check for white space or line breakers before the lengthBreak
- int startIndex = textIndex;
- int index = textIndex;
- int word_start = textIndex;
- bool prevBreak = true;
-
- trailingCount = 0;
-
- while (index < stop)
- {
- int prevText = index;
- char currChar = textInput[index++];
- bool currBreak = IsBreakChar(currChar);
-
- if (!currBreak && prevBreak)
- {
- word_start = prevText;
- }
-
- prevBreak = currBreak;
-
- if (index > startIndex + lengthBreak)
- {
- if (currBreak)
- {
- // eat the rest of the whitespace
- while (index < stop && IsBreakChar(textInput[index]))
- {
- index++;
- }
-
- trailingCount = index - prevText;
- }
- else
- {
- // backup until a whitespace (or 1 char)
- if (word_start == startIndex)
- {
- if (prevText > startIndex)
- {
- index = prevText;
- }
- }
- else
- {
- index = word_start;
- }
- }
- break;
- }
-
- if ('\n' == currChar)
- {
- int ret = index - startIndex;
- int lineBreakSize = 1;
- if (index < stop)
- {
- currChar = textInput[index++];
- if ('\r' == currChar)
- {
- ret = index - startIndex;
- ++lineBreakSize;
- }
- }
-
- trailingCount = lineBreakSize;
-
- return ret;
- }
-
- if ('\r' == currChar)
- {
- int ret = index - startIndex;
- int lineBreakSize = 1;
- if (index < stop)
- {
- currChar = textInput[index++];
- if ('\n' == currChar)
- {
- ret = index - startIndex;
- ++lineBreakSize;
- }
- }
-
- trailingCount = lineBreakSize;
-
- return ret;
- }
- }
-
- return index - startIndex;
- }
-
- private void BuildRects()
- {
- // Build character rects
- SKTextAlign align = _paint.TextAlign;
-
- for (int li = 0; li < _skiaLines.Count; li++)
- {
- var line = _skiaLines[li];
- float prevRight = TransformX(0, line.Width, align);
- double nextTop = line.Top + line.Height;
-
- if (li + 1 < _skiaLines.Count)
- {
- nextTop = _skiaLines[li + 1].Top;
- }
-
- for (int i = line.Start; i < line.Start + line.TextLength; i++)
- {
- var w = line.IsEmptyTrailingLine ? 0 : _advances[i];
-
- _rects.Add(new Rect(
- prevRight,
- line.Top,
- w,
- nextTop - line.Top));
- prevRight += w;
- }
- }
- }
-
- private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length)
- {
- IBrush result = null;
- int len = length = line.Start + line.Length - index;
-
- if (_foregroundBrushes.Any())
- {
- var bi = _foregroundBrushes.FindIndex(b =>
- b.Key.StartIndex <= index &&
- b.Key.EndIndex > index
- );
-
- if (bi > -1)
- {
- var match = _foregroundBrushes[bi];
-
- len = match.Key.EndIndex - index;
- result = match.Value;
-
- if (len > 0 && len < length)
- {
- length = len;
- }
- }
-
- int endIndex = index + length;
- int max = bi == -1 ? _foregroundBrushes.Count : bi;
- var next = _foregroundBrushes.Take(max)
- .Where(b => b.Key.StartIndex < endIndex &&
- b.Key.StartIndex > index)
- .OrderBy(b => b.Key.StartIndex)
- .FirstOrDefault();
-
- if (next.Value != null)
- {
- length = next.Key.StartIndex - index;
- }
- }
-
- return result;
- }
-
- private List GetRects()
- {
- if (Text.Length > _rects.Count)
- {
- BuildRects();
- }
-
- return _rects;
- }
-
- private void Rebuild()
- {
- var length = Text.Length;
-
- _lines.Clear();
- _rects.Clear();
- _skiaLines = new List();
-
- int curOff = 0;
- float curY = 0;
-
- var metrics = _paint.FontMetrics;
- var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
- var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
- var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
- var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
- var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
- var lastLineDescent = mBottom - mDescent;
-
- // This seems like the best measure of full vertical extent
- // matches Direct2D line height
- _lineHeight = mDescent - mAscent + metrics.Leading;
-
- // Rendering is relative to baseline
- _lineOffset = (-metrics.Ascent);
-
- string subString;
-
- float widthConstraint = double.IsPositiveInfinity(_constraint.Width)
- ? -1
- : (float)_constraint.Width;
-
- while(curOff < length)
- {
- float lineWidth = -1;
- int measured;
- int trailingnumber = 0;
-
- float constraint = -1;
-
- if (_wrapping == TextWrapping.Wrap)
- {
- constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint;
- if (constraint > MAX_LINE_WIDTH)
- constraint = MAX_LINE_WIDTH;
- }
-
- measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber);
- AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
- line.Start = curOff;
- line.TextLength = measured;
- subString = Text.Substring(line.Start, line.TextLength);
- lineWidth = MeasureText(line.Start, line.TextLength);
- line.Length = measured - trailingnumber;
- line.Width = lineWidth;
- line.Height = _lineHeight;
- line.Top = curY;
-
- _skiaLines.Add(line);
-
- curY += _lineHeight;
- curY += mLeading;
- curOff += measured;
-
- //if this is the last line and there are trailing newline characters then
- //insert a additional line
- if (curOff >= length)
- {
- var subStringMinusNewlines = subString.TrimEnd('\n', '\r');
- var lengthDiff = subString.Length - subStringMinusNewlines.Length;
- if (lengthDiff > 0)
- {
- AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine();
- lastLine.TextLength = lengthDiff;
- lastLine.Start = curOff - lengthDiff;
- var lastLineWidth = MeasureText(line.Start, line.TextLength);
- lastLine.Length = 0;
- lastLine.Width = lastLineWidth;
- lastLine.Height = _lineHeight;
- lastLine.Top = curY;
- lastLine.IsEmptyTrailingLine = true;
-
- _skiaLines.Add(lastLine);
-
- curY += _lineHeight;
- curY += mLeading;
- }
- }
- }
-
- // Now convert to Avalonia data formats
- _lines.Clear();
- float maxX = 0;
-
- for (var c = 0; c < _skiaLines.Count; c++)
- {
- var w = _skiaLines[c].Width;
- if (maxX < w)
- maxX = w;
-
- _lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height));
- }
-
- if (_skiaLines.Count == 0)
- {
- _lines.Add(new FormattedTextLine(0, _lineHeight));
- _bounds = new Rect(0, 0, 0, _lineHeight);
- }
- else
- {
- var lastLine = _skiaLines[_skiaLines.Count - 1];
- _bounds = new Rect(0, 0, maxX, lastLine.Top + lastLine.Height);
-
- if (double.IsPositiveInfinity(Constraint.Width))
- {
- return;
- }
-
- switch (_paint.TextAlign)
- {
- case SKTextAlign.Center:
- _bounds = new Rect(Constraint).CenterRect(_bounds);
- break;
- case SKTextAlign.Right:
- _bounds = new Rect(
- Constraint.Width - _bounds.Width,
- 0,
- _bounds.Width,
- _bounds.Height);
- break;
- }
- }
- }
-
- private float MeasureText(int start, int length)
- {
- var width = 0f;
-
- for (int i = start; i < start + length; i++)
- {
- var advance = _advances[i];
-
- width += advance;
- }
-
- return width;
- }
-
- private void UpdateGlyphInfo(string text, GlyphTypeface glyphTypeface, float fontSize)
- {
- var glyphs = new ushort[text.Length];
- var advances = new float[text.Length];
-
- var scale = fontSize / glyphTypeface.DesignEmHeight;
- var width = 0f;
- var characters = text.AsSpan();
-
- for (int i = 0; i < characters.Length; i++)
- {
- var c = characters[i];
- float advance;
- ushort glyph;
-
- switch (c)
- {
- case (char)0:
- {
- glyph = glyphTypeface.GetGlyph(0x200B);
- advance = 0;
- break;
- }
- case '\t':
- {
- glyph = glyphTypeface.GetGlyph(' ');
- advance = glyphTypeface.GetGlyphAdvance(glyph) * scale * 4;
- break;
- }
- default:
- {
- glyph = glyphTypeface.GetGlyph(c);
- advance = glyphTypeface.GetGlyphAdvance(glyph) * scale;
- break;
- }
- }
-
- glyphs[i] = glyph;
- advances[i] = advance;
-
- width += advance;
- }
-
- _glyphs = new ReadOnlySlice(glyphs);
- _advances = new ReadOnlySlice(advances);
- }
-
- private float TransformX(float originX, float lineWidth, SKTextAlign align)
- {
- float x = 0;
-
- if (align == SKTextAlign.Left)
- {
- x = originX;
- }
- else
- {
- double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
- Constraint.Width :
- _bounds.Width;
-
- switch (align)
- {
- case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break;
- case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break;
- }
- }
-
- return x;
- }
-
- private void SetForegroundBrush(IBrush brush, int startIndex, int length)
- {
- var key = new FBrushRange(startIndex, length);
- int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key));
-
- if (index > -1)
- {
- _foregroundBrushes.RemoveAt(index);
- }
-
- if (brush != null)
- {
- brush = brush.ToImmutable();
- _foregroundBrushes.Insert(0, new KeyValuePair(key, brush));
- }
- }
-
- private struct AvaloniaFormattedTextLine
- {
- public float Height;
- public int Length;
- public int Start;
- public int TextLength;
- public float Top;
- public float Width;
- public bool IsEmptyTrailingLine;
- };
-
- private struct FBrushRange
- {
- public FBrushRange(int startIndex, int length)
- {
- StartIndex = startIndex;
- Length = length;
- }
-
- public int EndIndex => StartIndex + Length;
-
- public int Length { get; private set; }
-
- public int StartIndex { get; private set; }
-
- public bool Intersects(int index, int len) =>
- (index + len) > StartIndex &&
- (StartIndex + Length) > index;
-
- public override string ToString()
- {
- return $"{StartIndex}-{EndIndex}";
- }
- }
- }
-}
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index 9601fece25d..5b6e5af60f7 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -64,6 +64,8 @@ public GlyphTypefaceImpl(SKTypeface typeface, bool isFakeBold = false, bool isFa
public SKTypeface Typeface { get; }
+ public int ReplacementCodepoint { get; }
+
///
public short DesignEmHeight { get; }
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index c3ac5e17744..af3b570fd7c 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -37,19 +37,6 @@ public PlatformRenderInterface(ISkiaGpu skiaGpu, long? maxResourceBytes = null)
_skiaGpu = new GlSkiaGpu(gl, maxResourceBytes);
}
- ///
- public IFormattedTextImpl CreateFormattedText(
- string text,
- Typeface typeface,
- double fontSize,
- TextAlignment textAlignment,
- TextWrapping wrapping,
- Size constraint,
- IReadOnlyList spans)
- {
- return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans);
- }
-
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);
@@ -208,7 +195,7 @@ public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, Pi
///
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
- var count = glyphRun.GlyphIndices.Length;
+ var count = glyphRun.GlyphIndices.Count;
var textBlobBuilder = s_textBlobBuilderThreadLocal.Value;
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
@@ -224,11 +211,18 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
- if (glyphRun.GlyphOffsets.IsEmpty)
+ if (glyphRun.GlyphOffsets == null)
{
if (glyphTypeface.IsFixedPitch)
{
- textBlobBuilder.AddRun(glyphRun.GlyphIndices.Buffer.Span, s_font);
+ var buffer = textBlobBuilder.AllocateRun(s_font, glyphRun.GlyphIndices.Count, 0, 0);
+
+ var glyphs = buffer.GetGlyphSpan();
+
+ for (int i = 0; i < glyphs.Length; i++)
+ {
+ glyphs[i] = glyphRun.GlyphIndices[i];
+ }
textBlob = textBlobBuilder.Build();
}
@@ -244,7 +238,7 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
positions[i] = (float)width;
- if (glyphRun.GlyphAdvances.IsEmpty)
+ if (glyphRun.GlyphAdvances == null)
{
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
@@ -254,7 +248,12 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
}
}
- buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+ var glyphs = buffer.GetGlyphSpan();
+
+ for (int i = 0; i < glyphs.Length; i++)
+ {
+ glyphs[i] = glyphRun.GlyphIndices[i];
+ }
textBlob = textBlobBuilder.Build();
}
@@ -273,7 +272,7 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
- if (glyphRun.GlyphAdvances.IsEmpty)
+ if (glyphRun.GlyphAdvances == null)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
@@ -283,7 +282,12 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
}
}
- buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+ var glyphs = buffer.GetGlyphSpan();
+
+ for (int i = 0; i < glyphs.Length; i++)
+ {
+ glyphs[i] = glyphRun.GlyphIndices[i];
+ }
textBlob = textBlobBuilder.Build();
}
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index 5cf72e2ce8a..c4d11f46139 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -1,146 +1,139 @@
using System;
using System.Globalization;
using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
+using GlyphInfo = HarfBuzzSharp.GlyphInfo;
namespace Avalonia.Skia
{
internal class TextShaperImpl : ITextShaperImpl
{
- public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
+ CultureInfo culture, sbyte bidiLevel)
{
using (var buffer = new Buffer())
{
- FillBuffer(buffer, text);
-
- buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
+ buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
+ MergeBreakPair(buffer);
+
buffer.GuessSegmentProperties();
- var glyphTypeface = typeface.GlyphTypeface;
+ buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
+
+ buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
- var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;
+ var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font;
font.Shape(buffer);
+ if (buffer.Direction == Direction.RightToLeft)
+ {
+ buffer.Reverse();
+ }
+
font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX;
var bufferLength = buffer.Length;
+ var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
+
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
- var glyphIndices = new ushort[bufferLength];
-
- var clusters = new ushort[bufferLength];
+ for (var i = 0; i < bufferLength; i++)
+ {
+ var sourceInfo = glyphInfos[i];
- double[] glyphAdvances = null;
+ var glyphIndex = (ushort)sourceInfo.Codepoint;
- Vector[] glyphOffsets = null;
+ var glyphCluster = (int)sourceInfo.Cluster;
- for (var i = 0; i < bufferLength; i++)
- {
- glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;
+ var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
- clusters[i] = (ushort)glyphInfos[i].Cluster;
+ var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
- if (!glyphTypeface.IsFixedPitch)
- {
- SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
- }
+ var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
- SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
+ shapedBuffer[i] = targetInfo;
}
- return new GlyphRun(glyphTypeface, fontRenderingEmSize,
- new ReadOnlySlice(glyphIndices),
- new ReadOnlySlice(glyphAdvances),
- new ReadOnlySlice(glyphOffsets),
- text,
- new ReadOnlySlice(clusters),
- buffer.Direction == Direction.LeftToRight ? 0 : 1);
+ return shapedBuffer;
}
}
- private static void FillBuffer(Buffer buffer, ReadOnlySlice text)
+ private static void MergeBreakPair(Buffer buffer)
{
- buffer.ContentType = ContentType.Unicode;
+ var length = buffer.Length;
- var i = 0;
+ var glyphInfos = buffer.GetGlyphInfoSpan();
+
+ var second = glyphInfos[length - 1];
- while (i < text.Length)
+ if (!new Codepoint((int)second.Codepoint).IsBreakChar)
{
- var codepoint = Codepoint.ReadAt(text, i, out var count);
+ return;
+ }
- var cluster = (uint)(text.Start + i);
+ if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
+ {
+ var first = glyphInfos[length - 2];
+
+ first.Codepoint = '\u200C';
+ second.Codepoint = '\u200C';
+ second.Cluster = first.Cluster;
- if (codepoint.IsBreakChar)
+ unsafe
{
- if (i + 1 < text.Length)
+ fixed (GlyphInfo* p = &glyphInfos[length - 2])
{
- var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _);
-
- if (nextCodepoint == '\n' && codepoint == '\r')
- {
- count++;
-
- buffer.Add('\u200C', cluster);
-
- buffer.Add('\u200D', cluster);
- }
- else
- {
- buffer.Add('\u200C', cluster);
- }
+ *p = first;
}
- else
+
+ fixed (GlyphInfo* p = &glyphInfos[length - 1])
{
- buffer.Add('\u200C', cluster);
+ *p = second;
}
}
- else
+ }
+ else
+ {
+ second.Codepoint = '\u200C';
+
+ unsafe
{
- buffer.Add(codepoint, cluster);
+ fixed (GlyphInfo* p = &glyphInfos[length - 1])
+ {
+ *p = second;
+ }
}
-
- i += count;
}
}
- private static void SetOffset(ReadOnlySpan glyphPositions, int index, double textScale,
- ref Vector[] offsetBuffer)
+ private static Vector GetGlyphOffset(ReadOnlySpan glyphPositions, int index, double textScale)
{
var position = glyphPositions[index];
- if (position.XOffset == 0 && position.YOffset == 0)
- {
- return;
- }
-
- offsetBuffer ??= new Vector[glyphPositions.Length];
-
var offsetX = position.XOffset * textScale;
var offsetY = position.YOffset * textScale;
- offsetBuffer[index] = new Vector(offsetX, offsetY);
+ return new Vector(offsetX, offsetY);
}
- private static void SetAdvance(ReadOnlySpan glyphPositions, int index, double textScale,
- ref double[] advanceBuffer)
+ private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale)
{
- advanceBuffer ??= new double[glyphPositions.Length];
-
// Depends on direction of layout
- // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
- advanceBuffer[index] = glyphPositions[index].XAdvance * textScale;
+ // glyphPositions[index].YAdvance * textScale;
+ return glyphPositions[index].XAdvance * textScale;
}
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index eef44161013..c32c58605fd 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -115,25 +115,6 @@ public static void Initialize()
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
- public IFormattedTextImpl CreateFormattedText(
- string text,
- Typeface typeface,
- double fontSize,
- TextAlignment textAlignment,
- TextWrapping wrapping,
- Size constraint,
- IReadOnlyList spans)
- {
- return new FormattedTextImpl(
- text,
- typeface,
- fontSize,
- textAlignment,
- wrapping,
- constraint,
- spans);
- }
-
public IRenderTarget CreateRenderTarget(IEnumerable