diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs
index 2901013cea..0377993d2c 100644
--- a/samples/ControlCatalog/Pages/PointersPage.cs
+++ b/samples/ControlCatalog/Pages/PointersPage.cs
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Documents;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
@@ -131,7 +132,7 @@ public class PointersPage : Decorator
{
public PointerIntermediatePointsTab()
{
- this[TextBlock.ForegroundProperty] = Brushes.Black;
+ this[TextElement.ForegroundProperty] = Brushes.Black;
var slider = new Slider
{
Margin = new Thickness(5),
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 923b51814f..429c4776d5 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -60,6 +60,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/TextFormatterPage.axaml b/samples/RenderDemo/Pages/TextFormatterPage.axaml
new file mode 100644
index 0000000000..4edf0852a2
--- /dev/null
+++ b/samples/RenderDemo/Pages/TextFormatterPage.axaml
@@ -0,0 +1,7 @@
+
+
diff --git a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
new file mode 100644
index 0000000000..92eb2e7dec
--- /dev/null
+++ b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
@@ -0,0 +1,118 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+
+namespace RenderDemo.Pages
+{
+ public class TextFormatterPage : UserControl
+ {
+ private TextLine _textLine;
+
+ public TextFormatterPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ _textLine?.Draw(context, new Point());
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black,
+ baselineAlignment: BaselineAlignment.Center);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
+
+ var control = new Button { Content = new TextBlock { Text = "ClickMe" } };
+
+ Content = control;
+
+ var textSource = new CustomTextSource(control, defaultRunProperties);
+
+ control.Measure(Size.Infinity);
+
+ _textLine =
+ TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
+
+ return base.MeasureOverride(availableSize);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var currentX = 0d;
+
+ foreach (var textRun in _textLine.TextRuns)
+ {
+ if (textRun is ControlRun controlRun)
+ {
+ controlRun.Control.Arrange(new Rect(new Point(currentX, 0), controlRun.Size));
+ }
+
+ if (textRun is DrawableTextRun drawableTextRun)
+ {
+ currentX += drawableTextRun.Size.Width;
+ }
+ }
+
+ return finalSize;
+ }
+
+ private class CustomTextSource : ITextSource
+ {
+ private readonly Control _control;
+ private readonly TextRunProperties _defaultProperties;
+ private readonly string _text = "<-Hello World->";
+
+ public CustomTextSource(Control control, TextRunProperties defaultProperties)
+ {
+ _control = control;
+ _defaultProperties = defaultProperties;
+ }
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ if (textSourceIndex >= _text.Length * 2 + TextRun.DefaultTextSourceLength)
+ {
+ return null;
+ }
+
+ if (textSourceIndex == _text.Length)
+ {
+ return new ControlRun(_control, _defaultProperties);
+ }
+
+ return new TextCharacters(_text.AsMemory(), _defaultProperties);
+ }
+ }
+
+ private class ControlRun : DrawableTextRun
+ {
+ private readonly Control _control;
+
+ public ControlRun(Control control, TextRunProperties properties)
+ {
+ _control = control;
+ Properties = properties;
+ }
+
+ public Control Control => _control;
+ public override Size Size => _control.DesiredSize;
+ public override double Baseline => 0;
+ public override TextRunProperties? Properties { get; }
+
+ public override void Draw(DrawingContext drawingContext, Point origin)
+ {
+ // noop
+ }
+ }
+ }
+}
diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj
index 54d5ca4b3b..3d5aee49e9 100644
--- a/samples/RenderDemo/RenderDemo.csproj
+++ b/samples/RenderDemo/RenderDemo.csproj
@@ -5,6 +5,10 @@
+
+ TextFormatterPage.axaml
+ Code
+
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
index e0dfa49a44..1d58c465a0 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
@@ -195,9 +195,9 @@
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
- TextBlock.FontFamily="{TemplateBinding FontFamily}"
- TextBlock.FontSize="{TemplateBinding FontSize}"
- TextBlock.FontWeight="{TemplateBinding FontWeight}" />
+ TextElement.FontFamily="{TemplateBinding FontFamily}"
+ TextElement.FontSize="{TemplateBinding FontSize}"
+ TextElement.FontWeight="{TemplateBinding FontWeight}" />
@@ -216,25 +216,25 @@
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
CornerRadius="{TemplateBinding CornerRadius}"
- TextBlock.FontFamily="{TemplateBinding FontFamily}"
- TextBlock.FontSize="{TemplateBinding FontSize}"
- TextBlock.FontWeight="{TemplateBinding FontWeight}" />
+ TextElement.FontFamily="{TemplateBinding FontFamily}"
+ TextElement.FontSize="{TemplateBinding FontSize}"
+ TextElement.FontWeight="{TemplateBinding FontWeight}" />
@@ -151,7 +151,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"/>
@@ -196,7 +196,7 @@
diff --git a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
index a9a03c8ed5..47398966f7 100644
--- a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
@@ -24,7 +24,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
index ce20a1a165..2d65ea2b7b 100644
--- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
@@ -146,7 +146,7 @@
SplitButton /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter">
-
+
diff --git a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
index b14a239c35..17fb2af16c 100644
--- a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
@@ -18,7 +18,7 @@
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
index 533fabfb44..7a0f9af83d 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
@@ -45,37 +45,37 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
index a9c8281cf0..0d1dd03c6e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
@@ -61,17 +61,17 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
index 12e148d2f9..649a186c7e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
@@ -29,7 +29,7 @@
@@ -165,7 +165,7 @@
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius}"/>
@@ -212,7 +212,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index c5bc7b6a38..24bdbca740 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -110,7 +110,7 @@
BorderThickness="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
- TextBlock.Foreground="{DynamicResource ExpanderForeground}" />
+ TextElement.Foreground="{DynamicResource ExpanderForeground}" />
@@ -49,7 +49,7 @@
@@ -57,7 +57,7 @@
@@ -65,7 +65,7 @@
@@ -73,7 +73,7 @@
@@ -81,7 +81,7 @@
@@ -89,6 +89,6 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
index 831537f578..09f11d9c11 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
@@ -218,7 +218,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
index 5ee2dc527f..fd3e3b0ed6 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
@@ -45,7 +45,7 @@
-
@@ -107,7 +107,7 @@
-
@@ -208,7 +208,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
index 694e9ef579..0b9bfcb200 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
@@ -36,9 +36,9 @@
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
- TextBlock.FontFamily="{TemplateBinding FontFamily}"
- TextBlock.FontSize="{TemplateBinding FontSize}"
- TextBlock.FontWeight="{TemplateBinding FontWeight}" />
+ TextElement.FontFamily="{TemplateBinding FontFamily}"
+ TextElement.FontSize="{TemplateBinding FontSize}"
+ TextElement.FontWeight="{TemplateBinding FontWeight}" />
@@ -69,7 +69,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
index 9aa73fc52e..6733aa39fa 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
@@ -51,7 +51,7 @@
ContentTemplate="{TemplateBinding HeaderTemplate}"
Margin="{DynamicResource TimePickerTopHeaderMargin}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
- TextBlock.Foreground="{DynamicResource TimePickerHeaderForeground}"
+ TextElement.Foreground="{DynamicResource TimePickerHeaderForeground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
@@ -77,7 +77,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
@@ -139,7 +139,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
index e66392113f..059e041e25 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
@@ -107,7 +107,7 @@
@@ -116,7 +116,7 @@
@@ -125,7 +125,7 @@
@@ -134,7 +134,7 @@
@@ -143,7 +143,7 @@
@@ -152,7 +152,7 @@
@@ -161,7 +161,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
index 2ff5284d0d..8d14c2d972 100644
--- a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index b725993b44..00246272e8 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -67,6 +67,8 @@ MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableRadialG
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.Immutable.ImmutableSolidColorBrush..ctor(Avalonia.Media.Color, System.Double)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Media.Immutable.ImmutableTileBrush..ctor(Avalonia.Media.AlignmentX, Avalonia.Media.AlignmentY, Avalonia.RelativeRect, System.Double, Avalonia.RelativeRect, Avalonia.Media.Stretch, Avalonia.Media.TileMode, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.DrawableTextRun.Baseline' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.DrawableTextRun.Baseline.get()' is abstract in the implementation but is missing 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.
CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
@@ -85,7 +87,7 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextC
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.
-CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Collections.Generic.List Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextCollapsingProperties.Style.get()' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.TextCollapsingStyle' 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.
@@ -94,8 +96,10 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLayout.Size.get()' 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.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.Length' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading' is abstract in the implementation but is missing in the contract.
@@ -108,18 +112,23 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextLine.GetTextBounds(System.Int32, System.Int32)' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.Length.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLine.LineMetrics.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextRange Avalonia.Media.TextFormatting.TextLine.TextRange.get()' does not exist in the implementation but it does exist in the contract.
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 System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextLineBreak.RemainingCharacters.get()' 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.
@@ -130,8 +139,6 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection.get()' is abstract in the implementation but is missing in the contract.
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.
CannotSealType : Type 'Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis..ctor(System.Double, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
@@ -171,9 +178,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.
-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.TextFormatting.ShapedBuffer Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.TextFormatting.TextShaperOptions)' 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: 177
+Total Issues: 184
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index 1cac3243e3..7bdf59def0 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -56,7 +56,7 @@ namespace Avalonia.Media
FlowDirection flowDirection,
Typeface typeface,
double emSize,
- IBrush foreground)
+ IBrush? foreground)
{
if (culture is null)
{
@@ -160,7 +160,7 @@ namespace Avalonia.Media
/// 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)
+ public void SetForegroundBrush(IBrush? foregroundBrush, int startIndex, int count)
{
var limit = ValidateRange(startIndex, count);
for (var i = startIndex; i < limit;)
@@ -768,7 +768,7 @@ namespace Avalonia.Media
// as a result of the next line measurement
// maybe there is no next line at all
- if (Position + Current.TextRange.Length < _that._text.Length)
+ if (Position + Current.Length < _that._text.Length)
{
bool nextLineFits;
@@ -780,7 +780,7 @@ namespace Avalonia.Media
{
_nextLine = FormatLine(
_textSource,
- Position + Current.TextRange.Length,
+ Position + Current.Length,
MaxLineLength(_lineCount + 1),
_that._defaultParaProps,
currentLineBreak
@@ -819,7 +819,7 @@ namespace Avalonia.Media
_previousHeight = Current.Height;
- Length = Current.TextRange.Length;
+ Length = Current.Length;
_previousLineBreak = currentLineBreak;
@@ -839,17 +839,17 @@ namespace Avalonia.Media
lineBreak
);
- if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.TextRange.Length > 0)
+ if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.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);
+ Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.Length <= _that._text.Length + 1);
var thatFormatRider = new SpanRider(
_that._formatRuns,
_that._latestPosition,
- Math.Min(textSourcePosition + line.TextRange.Length - 1, _that._text.Length - 1)
+ Math.Min(textSourcePosition + line.Length - 1, _that._text.Length - 1)
);
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
index 2dced2252e..2d7bd17b20 100644
--- a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
+++ b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
@@ -10,6 +10,16 @@ namespace Avalonia.Media
///
public class TextDecorationCollection : AvaloniaList
{
+ public TextDecorationCollection()
+ {
+
+ }
+
+ public TextDecorationCollection(IEnumerable textDecorations) : base(textDecorations)
+ {
+
+ }
+
///
/// Parses a string.
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
index 3757a4506a..e63ed421ba 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
@@ -9,6 +9,11 @@
/// Gets the size.
///
public abstract Size Size { get; }
+
+ ///
+ /// Run baseline in ratio relative to run height
+ ///
+ public abstract double Baseline { get; }
///
/// Draws the at the given origin.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
index 1b0feaa718..97df87d3d9 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@@ -30,10 +29,10 @@ namespace Avalonia.Media.TextFormatting
if (runText.IsEmpty)
{
- return new TextEndOfParagraph();
+ return null;
}
- var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
+ var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
}
@@ -42,17 +41,18 @@ namespace Avalonia.Media.TextFormatting
/// Creates a span of text run properties that has modifier applied.
///
/// The text to create the properties for.
+ /// The first text source index.
/// The default text properties.
/// The text properties modifier.
///
/// The created text style run.
///
- private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
+ private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
{
- return new ValueSpan(text.Start, text.Length, defaultProperties);
+ return new ValueSpan(firstTextSourceIndex, text.Length, defaultProperties);
}
var currentProperties = defaultProperties;
@@ -69,28 +69,28 @@ namespace Avalonia.Media.TextFormatting
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
- if (textRange.Start + textRange.Length <= text.Start)
+ if (textRange.Start + textRange.Length <= firstTextSourceIndex)
{
continue;
}
- if (textRange.Start > text.End)
+ if (textRange.Start > firstTextSourceIndex + text.Length)
{
length = text.Length;
break;
}
- if (textRange.Start > text.Start)
+ if (textRange.Start > firstTextSourceIndex)
{
if (propertiesOverride.Value != currentProperties)
{
- length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
+ length = Math.Min(Math.Abs(textRange.Start - firstTextSourceIndex), text.Length);
break;
}
}
- length += Math.Max(0, textRange.Start + textRange.Length - text.Start);
+ length = Math.Max(0, textRange.Start + textRange.Length - firstTextSourceIndex);
if (hasOverride)
{
@@ -116,12 +116,7 @@ namespace Avalonia.Media.TextFormatting
length = text.Length;
}
- if (length != text.Length)
- {
- text = text.Take(length);
- }
-
- return new ValueSpan(text.Start, length, currentProperties);
+ return new ValueSpan(firstTextSourceIndex, length, currentProperties);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index fb85766003..53287a264d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -38,6 +38,8 @@ namespace Avalonia.Media.TextFormatting
public FontMetrics FontMetrics { get; }
+ public override double Baseline => -FontMetrics.Ascent;
+
public override Size Size => GlyphRun.Size;
public GlyphRun GlyphRun
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs
new file mode 100644
index 0000000000..a0b51671f0
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// The bounding rectangle of a range of characters
+ ///
+ public sealed class TextBounds
+ {
+ ///
+ /// Constructing TextBounds object
+ ///
+ internal TextBounds(Rect bounds, FlowDirection flowDirection)
+ {
+ Rectangle = bounds;
+ FlowDirection = flowDirection;
+ }
+
+ ///
+ /// Bounds rectangle
+ ///
+ public Rect Rectangle { get; }
+
+ ///
+ /// Text flow direction inside the boundary rectangle
+ ///
+ public FlowDirection FlowDirection { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index c4b2dfb3a5..ab72601c3e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of .
///
/// The shapeable text characters.
- internal IList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel,
+ internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List(2);
@@ -72,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
- if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
+ if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
- if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
+ if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -89,7 +89,7 @@ namespace Avalonia.Media.TextFormatting
if (previousTypeface is not null)
{
- if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
+ if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -118,7 +118,7 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
- if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
+ if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
@@ -152,14 +152,19 @@ namespace Avalonia.Media.TextFormatting
///
/// The text.
/// The typeface that is used to find matching characters.
+ ///
/// The shapeable length.
///
///
- protected static bool TryGetShapeableLength(ReadOnlySlice text, Typeface typeface, out int length,
+ protected static bool TryGetShapeableLength(
+ ReadOnlySlice text,
+ Typeface typeface,
+ Typeface? defaultTypeface,
+ out int length,
out Script script)
{
length = 0;
- script = Script.Unknown;
+ script = Script.Unknown;
if (text.Length == 0)
{
@@ -167,6 +172,7 @@ namespace Avalonia.Media.TextFormatting
}
var font = typeface.GlyphTypeface;
+ var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
@@ -176,6 +182,11 @@ namespace Avalonia.Media.TextFormatting
var currentScript = currentGrapheme.FirstCodepoint.Script;
+ if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+ {
+ break;
+ }
+
//Stop at the first missing glyph
if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
index a46f9537d0..f677617b14 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line.
///
/// Text line to collapse.
- public abstract IReadOnlyList? Collapse(TextLine textLine);
+ public abstract List? Collapse(TextLine textLine);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
index 2031c2ec99..5a2169630b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
@@ -3,13 +3,11 @@ using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
{
- internal class TextEllipsisHelper
+ internal static class TextEllipsisHelper
{
- public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+ public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
- var shapedTextRuns = textLine.TextRuns as List;
-
- if (shapedTextRuns is null)
+ if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
{
return null;
}
@@ -17,74 +15,103 @@ namespace Avalonia.Media.TextFormatting
var runIndex = 0;
var currentWidth = 0.0;
var collapsedLength = 0;
- var textRange = textLine.TextRange;
var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{
- return new List(0);
+ //Not enough space to fit in the symbol
+ return new List(0);
}
var availableWidth = properties.Width - shapedSymbol.Size.Width;
- while (runIndex < shapedTextRuns.Count)
+ while (runIndex < textRuns.Count)
{
- var currentRun = shapedTextRuns[runIndex];
-
- currentWidth += currentRun.Size.Width;
+ var currentRun = textRuns[runIndex];
- if (currentWidth > availableWidth)
+ switch (currentRun)
{
- if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+ case ShapedTextCharacters shapedRun:
{
- if (isWordEllipsis && measuredLength < textRange.End)
+ currentWidth += shapedRun.Size.Width;
+
+ if (currentWidth > availableWidth)
{
- var currentBreakPosition = 0;
+ if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+ {
+ if (isWordEllipsis && measuredLength < textLine.Length)
+ {
+ var currentBreakPosition = 0;
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
- {
- var nextBreakPosition = lineBreaker.Current.PositionMeasure;
+ while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+ {
+ var nextBreakPosition = lineBreaker.Current.PositionMeasure;
- if (nextBreakPosition == 0)
- {
- break;
- }
+ if (nextBreakPosition == 0)
+ {
+ break;
+ }
- if (nextBreakPosition >= measuredLength)
- {
- break;
+ if (nextBreakPosition >= measuredLength)
+ {
+ break;
+ }
+
+ currentBreakPosition = nextBreakPosition;
+ }
+
+ measuredLength = currentBreakPosition;
}
+ }
+
+ collapsedLength += measuredLength;
- currentBreakPosition = nextBreakPosition;
+ var collapsedRuns = new List(textRuns.Count);
+
+ if (collapsedLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+
+ collapsedRuns.AddRange(splitResult.First);
}
- measuredLength = currentBreakPosition;
+ collapsedRuns.Add(shapedSymbol);
+
+ return collapsedRuns;
}
- }
- collapsedLength += measuredLength;
+ availableWidth -= currentRun.Size.Width;
- var shapedTextCharacters = new List(shapedTextRuns.Count);
+
+ break;
+ }
- if (collapsedLength > 0)
+ case { } drawableRun:
{
- var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, collapsedLength);
+ //The whole run needs to fit into available space
+ if (currentWidth + drawableRun.Size.Width > availableWidth)
+ {
+ var collapsedRuns = new List(textRuns.Count);
- shapedTextCharacters.AddRange(splitResult.First);
+ if (collapsedLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
- TextLineImpl.SortRuns(shapedTextCharacters);
- }
+ collapsedRuns.AddRange(splitResult.First);
+ }
- shapedTextCharacters.Add(shapedSymbol);
+ collapsedRuns.Add(shapedSymbol);
- return shapedTextCharacters;
+ return collapsedRuns;
+ }
+
+ break;
+ }
}
- availableWidth -= currentRun.Size.Width;
-
- collapsedLength += currentRun.GlyphRun.Characters.Length;
+ collapsedLength += currentRun.TextSourceLength;
runIndex++;
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 13ed850715..7c60f73b8d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
-using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@@ -17,22 +15,22 @@ namespace Avalonia.Media.TextFormatting
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection flowDirection;
TextLineBreak? nextLineBreak = null;
- List shapedRuns;
+ List drawableTextRuns;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
- out var textEndOfLine, out var textRange);
+ out var textEndOfLine, out var textSourceLength);
- if (previousLineBreak?.RemainingCharacters != null)
+ if (previousLineBreak?.RemainingRuns != null)
{
flowDirection = previousLineBreak.FlowDirection;
- shapedRuns = previousLineBreak.RemainingCharacters.ToList();
+ drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
nextLineBreak = previousLineBreak;
}
else
{
- shapedRuns = ShapeTextRuns(textRuns, paragraphProperties.FlowDirection,out flowDirection);
+ drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out flowDirection);
- if(nextLineBreak == null && textEndOfLine != null)
+ if (nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
}
@@ -44,10 +42,8 @@ namespace Avalonia.Media.TextFormatting
{
case TextWrapping.NoWrap:
{
- TextLineImpl.SortRuns(shapedRuns);
-
- textLine = new TextLineImpl(shapedRuns, textRange, paragraphWidth, paragraphProperties,
- flowDirection, nextLineBreak);
+ textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
+ paragraphWidth, paragraphProperties, flowDirection, nextLineBreak);
textLine.FinalizeLine();
@@ -56,7 +52,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
- textLine = PerformTextWrapping(shapedRuns, textRange, paragraphWidth, paragraphProperties,
+ textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
flowDirection, nextLineBreak);
break;
}
@@ -73,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
/// The text run's.
/// The length to split at.
/// The split text runs.
- internal static SplitResult> SplitShapedRuns(List textRuns, int length)
+ internal static SplitResult> SplitDrawableRuns(List textRuns, int length)
{
var currentLength = 0;
@@ -83,13 +79,14 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Text.Length < length)
{
- currentLength += currentRun.Text.Length;
+ currentLength += currentRun.TextSourceLength;
+
continue;
}
var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
- var first = new List(firstCount);
+ var first = new List(firstCount);
if (firstCount > 1)
{
@@ -103,7 +100,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Text.Length == length)
{
- var second = secondCount > 0 ? new List(secondCount) : null;
+ var second = secondCount > 0 ? new List(secondCount) : null;
if (second != null)
{
@@ -117,15 +114,20 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun);
- return new SplitResult>(first, second);
+ return new SplitResult>(first, second);
}
else
{
secondCount++;
- var second = new List(secondCount);
+ var second = new List(secondCount);
+
+ if (currentRun is not ShapedTextCharacters shapedTextCharacters)
+ {
+ throw new NotSupportedException("Only shaped runs can be split in between.");
+ }
- var split = currentRun.Split(length - currentLength);
+ var split = shapedTextCharacters.Split(length - currentLength);
first.Add(split.First);
@@ -136,32 +138,43 @@ namespace Avalonia.Media.TextFormatting
second.Add(textRuns[i + j]);
}
- return new SplitResult>(first, second);
+ return new SplitResult>(first, second);
}
}
- return new SplitResult>(textRuns, null);
+ return new SplitResult>(textRuns, null);
}
///
/// Shape specified text runs with specified paragraph embedding.
///
/// The text runs to shape.
- /// The paragraph embedding level.
+ /// The default paragraph properties.
/// The resolved flow direction.
///
/// A list of shaped text characters.
///
- private static List ShapeTextRuns(List textRuns,
- FlowDirection flowDirection, out FlowDirection resolvedFlowDirection)
+ private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties,
+ out FlowDirection resolvedFlowDirection)
{
- var shapedTextCharacters = new List();
+ var flowDirection = paragraphProperties.FlowDirection;
+ var drawableTextRuns = new List();
var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns)
{
- biDiData.Append(textRun.Text);
+ if (textRun.Text.IsEmpty)
+ {
+ var text = new char[textRun.TextSourceLength];
+
+ biDiData.Append(text);
+ }
+ else
+ {
+ biDiData.Append(textRun.Text);
+ }
+
}
var biDi = BidiAlgorithm.Instance.Value!;
@@ -173,68 +186,90 @@ namespace Avalonia.Media.TextFormatting
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
- var shapeableRuns = new List(textRuns.Count);
+ var processedRuns = new List(textRuns.Count);
foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{
- shapeableRuns.AddRange(coalescedRuns);
+ processedRuns.AddRange(coalescedRuns);
}
- for (var index = 0; index < shapeableRuns.Count; index++)
+ for (var index = 0; index < processedRuns.Count; index++)
{
- var currentRun = shapeableRuns[index];
- var groupedRuns = new List(2) { currentRun };
- var text = currentRun.Text;
- var start = currentRun.Text.Start;
- var length = currentRun.Text.Length;
- var bufferOffset = currentRun.Text.BufferOffset;
-
- while (index + 1 < shapeableRuns.Count)
- {
- var nextRun = shapeableRuns[index + 1];
-
- if (currentRun.CanShapeTogether(nextRun))
- {
- groupedRuns.Add(nextRun);
+ var currentRun = processedRuns[index];
- length += nextRun.Text.Length;
-
- if (start > nextRun.Text.Start)
+ switch (currentRun)
+ {
+ case DrawableTextRun drawableRun:
{
- start = nextRun.Text.Start;
+ drawableTextRuns.Add(drawableRun);
+
+ break;
}
- if (bufferOffset > nextRun.Text.BufferOffset)
+ case ShapeableTextCharacters shapeableRun:
{
- bufferOffset = nextRun.Text.BufferOffset;
- }
+ var groupedRuns = new List(2) { shapeableRun };
+ var text = currentRun.Text;
+ var start = currentRun.Text.Start;
+ var length = currentRun.Text.Length;
+ var bufferOffset = currentRun.Text.BufferOffset;
- text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset);
-
- index++;
+ while (index + 1 < processedRuns.Count)
+ {
+ if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
+ {
+ break;
+ }
- currentRun = nextRun;
+ if (shapeableRun.CanShapeTogether(nextRun))
+ {
+ groupedRuns.Add(nextRun);
- continue;
- }
+ length += nextRun.Text.Length;
- break;
- }
+ if (start > nextRun.Text.Start)
+ {
+ start = nextRun.Text.Start;
+ }
+
+ if (bufferOffset > nextRun.Text.BufferOffset)
+ {
+ bufferOffset = nextRun.Text.BufferOffset;
+ }
+
+ text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset);
- shapedTextCharacters.AddRange(ShapeTogether(groupedRuns, text));
+ index++;
+
+ shapeableRun = nextRun;
+
+ continue;
+ }
+
+ break;
+ }
+
+ var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
+ currentRun.Properties.FontRenderingEmSize,
+ shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
+
+ drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+
+ break;
+ }
+ }
}
- return shapedTextCharacters;
+ return drawableTextRuns;
}
private static IReadOnlyList ShapeTogether(
- IReadOnlyList textRuns, ReadOnlySlice text)
+ IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options)
{
var shapedRuns = new List(textRuns.Count);
var firstRun = textRuns[0];
- var shapedBuffer = TextShaper.Current.ShapeText(text, firstRun.Properties.Typeface.GlyphTypeface,
- firstRun.Properties.FontRenderingEmSize, firstRun.Properties.CultureInfo, firstRun.BidiLevel);
+ var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@@ -256,8 +291,8 @@ namespace Avalonia.Media.TextFormatting
/// The text characters to form from.
/// The bidi levels.
///
- private static IEnumerable> CoalesceLevels(
- IReadOnlyList textCharacters,
+ private static IEnumerable> CoalesceLevels(
+ IReadOnlyList textCharacters,
ReadOnlySlice levels)
{
if (levels.Length == 0)
@@ -275,7 +310,19 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < textCharacters.Count; i++)
{
var j = 0;
- currentRun = textCharacters[i];
+ currentRun = textCharacters[i] as TextCharacters;
+
+ if (currentRun == null)
+ {
+ var drawableRun = textCharacters[i];
+
+ yield return new[] { drawableRun };
+
+ levelIndex += drawableRun.TextSourceLength;
+
+ continue;
+ }
+
runText = currentRun.Text;
for (; j < runText.Length;)
@@ -330,18 +377,18 @@ namespace Avalonia.Media.TextFormatting
/// 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)
+ private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
+ out TextEndOfLine? endOfLine, out int textSourceLength)
{
- var length = 0;
+ textSourceLength = 0;
endOfLine = null;
- var textRuns = new List();
+ var textRuns = new List();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
@@ -349,8 +396,19 @@ namespace Avalonia.Media.TextFormatting
{
var textRun = textRunEnumerator.Current;
- if(textRun == null)
+ if (textRun == null)
+ {
+ break;
+ }
+
+ if (textRun is TextEndOfLine textEndOfLine)
{
+ endOfLine = textEndOfLine;
+
+ textRuns.Add(textRun);
+
+ textSourceLength += textRun.TextSourceLength;
+
break;
}
@@ -365,9 +423,7 @@ namespace Avalonia.Media.TextFormatting
textRuns.Add(splitResult);
- length += runLineBreak.PositionWrap;
-
- textRange = new TextRange(firstTextSourceIndex, length);
+ textSourceLength += runLineBreak.PositionWrap;
return textRuns;
}
@@ -376,16 +432,16 @@ namespace Avalonia.Media.TextFormatting
break;
}
- case TextEndOfLine textEndOfLine:
- endOfLine = textEndOfLine;
- break;
+ case DrawableTextRun drawableTextRun:
+ {
+ textRuns.Add(drawableTextRun);
+ break;
+ }
}
- length += textRun.Text.Length;
+ textSourceLength += textRun.TextSourceLength;
}
- textRange = new TextRange(firstTextSourceIndex, length);
-
return textRuns;
}
@@ -415,48 +471,74 @@ namespace Avalonia.Media.TextFormatting
return false;
}
- private static int MeasureLength(IReadOnlyList textRuns, TextRange textRange,
- double paragraphWidth)
+ private static bool TryMeasureLength(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
{
+ measuredLength = 0;
var currentWidth = 0.0;
- var lastCluster = textRange.Start;
+ var lastCluster = firstTextSourceIndex;
foreach (var currentRun in textRuns)
{
- for (var i = 0; i < currentRun.ShapedBuffer.Length; i++)
+ switch (currentRun)
{
- var glyphInfo = currentRun.ShapedBuffer[i];
+ case ShapedTextCharacters shapedTextCharacters:
+ {
+ for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
+ {
+ var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
- if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
- {
- var measuredLength = lastCluster - textRange.Start;
+ if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+ {
+ goto found;
+ }
- return measuredLength == 0 ? 1 : measuredLength;
- }
+ lastCluster = glyphInfo.GlyphCluster;
+ currentWidth += glyphInfo.GlyphAdvance;
+ }
+
+ break;
+ }
+
+ case { } drawableTextRun:
+ {
+ if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
+ {
+ goto found;
+ }
- lastCluster = glyphInfo.GlyphCluster;
- currentWidth += glyphInfo.GlyphAdvance;
+ lastCluster += currentRun.TextSourceLength;
+ currentWidth += currentRun.Size.Width;
+
+ break;
+ }
}
}
- return textRange.Length;
+ found:
+
+ measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
+
+ return measuredLength != 0;
}
///
/// Performs text wrapping returns a list of text lines.
///
///
- /// The text range that is covered by the text runs.
+ /// The first text source index.
/// 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,
+ private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
- var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
+ if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
+ {
+ measuredLength = 1;
+ }
var currentLength = 0;
@@ -568,9 +650,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
- var splitResult = SplitShapedRuns(textRuns, measuredLength);
-
- textRange = new TextRange(textRange.Start, measuredLength);
+ var splitResult = SplitDrawableRuns(textRuns, measuredLength);
var remainingCharacters = splitResult.Second;
@@ -583,9 +663,8 @@ namespace Avalonia.Media.TextFormatting
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
}
- TextLineImpl.SortRuns(splitResult.First);
-
- var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection,
+ var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
+ paragraphWidth, paragraphProperties, flowDirection,
lineBreak);
return textLine.FinalizeLine();
@@ -644,7 +723,9 @@ namespace Avalonia.Media.TextFormatting
var cultureInfo = textRun.Properties.CultureInfo;
- var shapedBuffer = textShaper.ShapeText(textRun.Text, glyphTypeface, fontRenderingEmSize, cultureInfo, (sbyte)flowDirection);
+ var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
+
+ var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
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 0ff127694b..e3bcdee014 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -162,7 +162,9 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in TextLines)
{
- if (textLine.TextRange.End < textPosition)
+ var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
+
+ if (end < textPosition)
{
currentY += textLine.Height;
@@ -193,187 +195,34 @@ namespace Avalonia.Media.TextFormatting
var result = new List(TextLines.Count);
var currentY = 0d;
- var currentPosition = 0;
- var currentRect = Rect.Empty;
foreach (var textLine in TextLines)
{
//Current line isn't covered.
- if (currentPosition + textLine.TextRange.Length <= start)
+ if (textLine.FirstTextSourceIndex + textLine.Length <= start)
{
currentY += textLine.Height;
- currentPosition += textLine.TextRange.Length;
continue;
}
- //The whole line is covered.
- if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length)
- {
- currentRect = new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace,
- textLine.Height);
-
- result.Add(currentRect);
-
- 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;
+ var textBounds = textLine.GetTextBounds(start, length);
- 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;
- }
- }
- }
-
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
- }
-
- var width = endX - startX;
+ foreach (var bounds in textBounds)
+ {
+ Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
- if (result.Count > 0 && MathUtilities.AreClose(currentRect.Top, currentY) &&
- MathUtilities.AreClose(currentRect.Right, startX))
- {
- result[result.Count - 1] = currentRect.WithWidth(currentRect.Width + width);
- }
- else
- {
- currentRect = new Rect(startX, currentY, width, textLine.Height);
-
- result.Add(currentRect);
- }
-
- if (currentRun.ShapedBuffer.IsLeftToRight)
+ if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
{
- if (nextRun != null)
- {
- if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length)
- {
- break;
- }
-
- currentPosition = nextRun.Text.End;
- }
- else
- {
- if (currentPosition >= start + length)
- {
- break;
- }
- }
+ result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
}
else
{
- if (currentPosition <= start)
- {
- break;
- }
- }
-
- if (!currentRun.ShapedBuffer.IsLeftToRight && currentPosition != currentRun.Text.Start)
- {
- endX += currentRun.GlyphRun.Size.Width - endOffset;
- }
-
- startX = endX;
+ result.Add(bounds.Rectangle.WithY(currentY));
+ }
}
- if (currentPosition == start || currentPosition == start + length)
- {
- break;
- }
-
- if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
+ if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)
{
break;
}
@@ -433,12 +282,13 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextLines[index];
- if (textLine.TextRange.Start + textLine.TextRange.Length < charIndex)
+ if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
{
continue;
}
- if (charIndex >= textLine.TextRange.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0))
+ if (charIndex >= textLine.FirstTextSourceIndex &&
+ charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{
return index;
}
@@ -451,11 +301,11 @@ namespace Avalonia.Media.TextFormatting
{
var (x, y) = point;
- var lastTrailingIndex = textLine.TextRange.Start + textLine.TextRange.Length;
+ var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
- if (x >= textLine.Width && textLine.TextRange.Length > 0 && textLine.NewLineLength > 0)
+ if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
{
lastTrailingIndex -= textLine.NewLineLength;
}
@@ -465,7 +315,7 @@ namespace Avalonia.Media.TextFormatting
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Bounds.Bottom;
- if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length)
+ if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
{
textPosition -= textLine.NewLineLength;
}
@@ -529,23 +379,21 @@ namespace Avalonia.Media.TextFormatting
/// Creates an empty text line.
///
/// The empty text line.
- private TextLine CreateEmptyTextLine(int startingIndex)
+ private TextLine CreateEmptyTextLine(int firstTextSourceIndex)
{
var flowDirection = _paragraphProperties.FlowDirection;
var properties = _paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
- var text = new ReadOnlySlice(s_empty, startingIndex, 1);
+ var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
- var glyphInfos = new[] { new GlyphInfo(glyph, startingIndex) };
+ var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
- var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
-
- var textRange = new TextRange(startingIndex, 1);
+ var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
- return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
+ return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
}
private IReadOnlyList CreateTextLines()
@@ -576,13 +424,13 @@ namespace Avalonia.Media.TextFormatting
_paragraphProperties, previousLine?.TextLineBreak);
#if DEBUG
- if (textLine.TextRange.Length == 0)
+ if (textLine.Length == 0)
{
throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
}
#endif
- currentPosition += textLine.TextRange.Length;
+ currentPosition += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
index 74c4573630..5a14eda245 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Media.TextFormatting
/// Text used as collapsing symbol.
/// Length of leading prefix.
/// width in which collapsing is constrained to
- /// text run properties of ellispis symbol
+ /// text run properties of ellipsis symbol
public TextLeadingPrefixCharacterEllipsis(
ReadOnlySlice ellipsis,
int prefixLength,
@@ -35,16 +35,14 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
- var shapedTextRuns = textLine.TextRuns as List;
-
- if (shapedTextRuns is null)
+ if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
{
return null;
}
@@ -55,84 +53,103 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width)
{
- return new List(0);
+ return new List(0);
}
// Overview of ellipsis structure
// Prefix length run | Ellipsis symbol | Post split run growing from the end |
var availableWidth = Width - shapedSymbol.Size.Width;
- while (runIndex < shapedTextRuns.Count)
+ while (runIndex < textRuns.Count)
{
- var currentRun = shapedTextRuns[runIndex];
-
- currentWidth += currentRun.Size.Width;
+ var currentRun = textRuns[runIndex];
- if (currentWidth > availableWidth)
+ switch (currentRun)
{
- currentRun.TryMeasureCharacters(availableWidth, out var measuredLength);
-
- var shapedTextCharacters = new List(shapedTextRuns.Count);
-
- if (measuredLength > 0)
+ case ShapedTextCharacters shapedRun:
{
- List? preSplitRuns = null;
- List? postSplitRuns = null;
+ currentWidth += currentRun.Size.Width;
- if (_prefixLength > 0)
+ if (currentWidth > availableWidth)
{
- var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength));
+ shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
- shapedTextCharacters.AddRange(splitResult.First);
+ var collapsedRuns = new List(textRuns.Count);
- TextLineImpl.SortRuns(shapedTextCharacters);
-
- preSplitRuns = splitResult.First;
- postSplitRuns = splitResult.Second;
- }
- else
- {
- postSplitRuns = shapedTextRuns;
- }
+ if (measuredLength > 0)
+ {
+ List? preSplitRuns = null;
+ List? postSplitRuns;
- shapedTextCharacters.Add(shapedSymbol);
+ if (_prefixLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
+ Math.Min(_prefixLength, measuredLength));
- if (measuredLength > _prefixLength && postSplitRuns is not null)
- {
- var availableSuffixWidth = availableWidth;
+ collapsedRuns.AddRange(splitResult.First);
- if (preSplitRuns is not null)
- {
- foreach (var run in preSplitRuns)
+ preSplitRuns = splitResult.First;
+ postSplitRuns = splitResult.Second;
+ }
+ else
{
- availableSuffixWidth -= run.Size.Width;
+ postSplitRuns = textRuns;
}
- }
- for (int i = postSplitRuns.Count - 1; i >= 0; i--)
- {
- var run = postSplitRuns[i];
+ collapsedRuns.Add(shapedSymbol);
- if (run.TryMeasureCharactersBackwards(availableSuffixWidth, out int suffixCount, out double suffixWidth))
+ if (measuredLength <= _prefixLength || postSplitRuns is null)
{
- availableSuffixWidth -= suffixWidth;
+ return collapsedRuns;
+ }
+
+ var availableSuffixWidth = availableWidth;
- if (suffixCount > 0)
+ if (preSplitRuns is not null)
+ {
+ foreach (var run in preSplitRuns)
{
- var splitSuffix = run.Split(run.TextSourceLength - suffixCount);
+ availableSuffixWidth -= run.Size.Width;
+ }
+ }
+
+ for (var i = postSplitRuns.Count - 1; i >= 0; i--)
+ {
+ var run = postSplitRuns[i];
- shapedTextCharacters.Add(splitSuffix.Second!);
+ switch (run)
+ {
+ case ShapedTextCharacters endShapedRun:
+ {
+ if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
+ out var suffixCount, out var suffixWidth))
+ {
+ availableSuffixWidth -= suffixWidth;
+
+ if (suffixCount > 0)
+ {
+ var splitSuffix =
+ endShapedRun.Split(run.TextSourceLength - suffixCount);
+
+ collapsedRuns.Add(splitSuffix.Second!);
+ }
+ }
+
+ break;
+ }
}
}
}
+ else
+ {
+ collapsedRuns.Add(shapedSymbol);
+ }
+
+ return collapsedRuns;
}
- }
- else
- {
- shapedTextCharacters.Add(shapedSymbol);
- }
- return shapedTextCharacters;
+ break;
+ }
}
availableWidth -= currentRun.Size.Width;
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index 130d0e9c39..1f69c15acc 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -16,13 +16,9 @@ namespace Avalonia.Media.TextFormatting
///
public abstract IReadOnlyList TextRuns { get; }
- ///
- /// Gets the text range that is covered by the line.
- ///
- ///
- /// The text range that is covered by the line.
- ///
- public abstract TextRange TextRange { get; }
+ public abstract int FirstTextSourceIndex { get; }
+
+ public abstract int Length { get; }
///
/// Gets the state of the line when broken by line breaking process.
@@ -189,6 +185,14 @@ namespace Avalonia.Media.TextFormatting
/// The after backspacing.
public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit);
+ ///
+ /// Get an array of bounding rectangles of a range of characters within a text line.
+ ///
+ /// index of first character of specified range
+ /// number of characters of the specified range
+ /// an array of bounding rectangles.
+ public abstract IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength);
+
///
/// Gets the text line offset x.
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
index be9661c2bf..ce35e47fbd 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
@@ -5,11 +5,11 @@ namespace Avalonia.Media.TextFormatting
public class TextLineBreak
{
public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight,
- IReadOnlyList? remainingCharacters = null)
+ IReadOnlyList? remainingRuns = null)
{
TextEndOfLine = textEndOfLine;
FlowDirection = flowDirection;
- RemainingCharacters = remainingCharacters;
+ RemainingRuns = remainingRuns;
}
///
@@ -23,8 +23,8 @@ namespace Avalonia.Media.TextFormatting
public FlowDirection FlowDirection { get; }
///
- /// Get the remaining shaped characters that were split up by the during the formatting process.
+ /// Get the remaining runs that were split up by the during the formatting process.
///
- public IReadOnlyList? RemainingCharacters { get; }
+ public IReadOnlyList? RemainingRuns { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index 49bee6e776..30e3728d1f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -1,32 +1,27 @@
using System;
using System.Collections.Generic;
-using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
- 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 List _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _flowDirection;
- public TextLineImpl(List textRuns, TextRange textRange, double paragraphWidth,
- TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
+ public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
+ TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{
- TextRange = textRange;
+ FirstTextSourceIndex = firstTextSourceIndex;
+ Length = length;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
- _textRuns = textRuns;
+ _textRuns = textRuns;
_paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties;
@@ -37,7 +32,10 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList TextRuns => _textRuns;
///
- public override TextRange TextRange { get; }
+ public override int FirstTextSourceIndex { get; }
+
+ ///
+ public override int Length { get; }
///
public override TextLineBreak? TextLineBreak { get; }
@@ -88,7 +86,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns)
{
- var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
+ var offsetY = GetBaselineOffset(this, textRun);
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
@@ -96,6 +94,30 @@ namespace Avalonia.Media.TextFormatting
}
}
+ private static double GetBaselineOffset(TextLine textLine, DrawableTextRun textRun)
+ {
+ var baseline = textRun.Baseline;
+ var baselineAlignment = textRun.Properties?.BaselineAlignment;
+
+ switch (baselineAlignment)
+ {
+ case BaselineAlignment.Top:
+ return 0;
+ case BaselineAlignment.Center:
+ return textLine.Height / 2 - textRun.Size.Height / 2;
+ case BaselineAlignment.Bottom:
+ return textLine.Height - textRun.Size.Height;
+ case BaselineAlignment.Baseline:
+ case BaselineAlignment.TextTop:
+ case BaselineAlignment.TextBottom:
+ case BaselineAlignment.Subscript:
+ case BaselineAlignment.Superscript:
+ return textLine.Baseline - baseline;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(baselineAlignment), baselineAlignment, null);
+ }
+ }
+
///
public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
{
@@ -108,47 +130,82 @@ namespace Avalonia.Media.TextFormatting
var collapsedRuns = collapsingProperties.Collapse(this);
- if (collapsedRuns is List shapedRuns)
+ if (collapsedRuns is null)
{
- var collapsedLine = new TextLineImpl(shapedRuns, TextRange, _paragraphWidth, _paragraphProperties, _flowDirection, TextLineBreak, true);
+ return this;
+ }
- if (shapedRuns.Count > 0)
- {
- collapsedLine.FinalizeLine();
- }
+ var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
+ _flowDirection, TextLineBreak, true);
- return collapsedLine;
+ if (collapsedRuns.Count > 0)
+ {
+ collapsedLine.FinalizeLine();
}
- return this;
+ return collapsedLine;
+
}
///
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
+ if (_textRuns.Count == 0)
+ {
+ return new CharacterHit();
+ }
+
distance -= Start;
-
+
if (distance <= 0)
{
// hit happens before the line, return the first position
var firstRun = _textRuns[0];
- return firstRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ if (firstRun is ShapedTextCharacters shapedTextCharacters)
+ {
+ return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ }
+
+ return _flowDirection == FlowDirection.LeftToRight ?
+ new CharacterHit(FirstTextSourceIndex) :
+ new CharacterHit(FirstTextSourceIndex + Length);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
+ var currentPosition = FirstTextSourceIndex;
- foreach (var run in _textRuns)
+ foreach (var currentRun in _textRuns)
{
- characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ break;
+ }
+ default:
+ {
+ if (distance < currentRun.Size.Width / 2)
+ {
+ characterHit = new CharacterHit(currentPosition);
+ }
+ else
+ {
+ characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength);
+ }
+ break;
+ }
+ }
- if (distance <= run.Size.Width)
+ if (distance <= currentRun.Size.Width)
{
break;
}
- distance -= run.Size.Width;
+ distance -= currentRun.Size.Width;
+ currentPosition += currentRun.TextSourceLength;
}
return characterHit;
@@ -158,91 +215,119 @@ namespace Avalonia.Media.TextFormatting
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
-
var currentDistance = Start;
+ var currentPosition = FirstTextSourceIndex;
GlyphRun? lastRun = null;
for (var index = 0; index < _textRuns.Count; index++)
{
var textRun = _textRuns[index];
- var currentRun = textRun.GlyphRun;
- if (lastRun != null)
+ switch (textRun)
{
- if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
- currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
- characterHit.TrailingLength == 0)
- {
- return currentDistance;
- }
- }
-
- //Look for a hit in within the current run
- if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.End)
- {
- var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
+ case ShapedTextCharacters shapedTextCharacters:
+ {
+ var currentRun = shapedTextCharacters.GlyphRun;
- return currentDistance + distance;
- }
+ if (lastRun != null)
+ {
+ if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
+ currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
+ characterHit.TrailingLength == 0)
+ {
+ return currentDistance;
+ }
+ }
- //Look at the left and right edge of the current run
- if (currentRun.IsLeftToRight)
- {
- if (lastRun == null || lastRun.IsLeftToRight)
- {
- if (characterIndex <= textRun.Text.Start)
- {
- return currentDistance;
- }
- }
- else
- {
- if (characterIndex == textRun.Text.Start)
- {
- return currentDistance;
- }
- }
+ //Look for a hit in within the current run
+ if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.Start + textRun.Text.Length)
+ {
+ var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
- 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;
- }
+ return currentDistance + distance;
+ }
- var nextRun = index + 1 < _textRuns.Count ? _textRuns[index + 1] : null;
+ //Look at the left and right edge of the current run
+ if (currentRun.IsLeftToRight)
+ {
+ if (lastRun == null || lastRun.IsLeftToRight)
+ {
+ if (characterIndex <= textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex == textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
+
+ 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] as ShapedTextCharacters :
+ null;
+
+ if (nextRun != null)
+ {
+ if (characterHit.FirstCharacterIndex == textRun.Text.End &&
+ nextRun.ShapedBuffer.IsLeftToRight)
+ {
+ return currentDistance;
+ }
+
+ if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex > textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ }
- if (nextRun != null)
- {
- if (characterHit.FirstCharacterIndex == textRun.Text.End && nextRun.ShapedBuffer.IsLeftToRight)
- {
- return currentDistance;
- }
+ lastRun = currentRun;
- if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
- {
- return currentDistance;
+ break;
}
- }
- else
- {
- if (characterIndex > textRun.Text.End)
+ default:
{
- return currentDistance;
+ if (characterIndex == currentPosition)
+ {
+ return currentDistance;
+ }
+
+ if (characterIndex == currentPosition + textRun.TextSourceLength)
+ {
+ return currentDistance + textRun.Size.Width;
+ }
+
+ break;
}
- }
}
//No hit hit found so we add the full width
- currentDistance += currentRun.Size.Width;
-
- lastRun = currentRun;
+ currentDistance += textRun.Size.Width;
+ currentPosition += textRun.TextSourceLength;
}
return currentDistance;
@@ -251,19 +336,38 @@ namespace Avalonia.Media.TextFormatting
///
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
+ if (_textRuns.Count == 0)
+ {
+ return new CharacterHit();
+ }
+
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit))
{
return nextCharacterHit;
}
+ var lastTextPosition = FirstTextSourceIndex + Length;
+
// Can't move, we're after the last character
- var runIndex = GetRunIndexAtCharacterIndex(TextRange.End, LogicalDirection.Forward);
+ var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition);
- var textRun = _textRuns[runIndex];
+ var currentRun = _textRuns[runIndex];
- characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+ break;
+ }
+ default:
+ {
+ characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+ break;
+ }
+ }
- return characterHit;
+ return characterHit;
}
///
@@ -274,9 +378,9 @@ namespace Avalonia.Media.TextFormatting
return previousCharacterHit;
}
- if (characterHit.FirstCharacterIndex <= TextRange.Start)
+ if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
{
- characterHit = new CharacterHit(TextRange.Start);
+ characterHit = new CharacterHit(FirstTextSourceIndex);
}
return characterHit; // Can't move, we're before the first character
@@ -289,9 +393,199 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
- public static void SortRuns(List textRuns)
+ public override IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength)
{
- textRuns.Sort(s_compareLogicalOrder);
+ if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex)
+ {
+ return Array.Empty();
+ }
+
+ var result = new List(TextRuns.Count);
+ var lastDirection = _flowDirection;
+ var currentDirection = lastDirection;
+ var currentPosition = 0;
+ var currentRect = Rect.Empty;
+ var startX = Start;
+
+ //A portion of the line is covered.
+ for (var index = 0; index < TextRuns.Count; index++)
+ {
+ var currentRun = TextRuns[index] as DrawableTextRun;
+
+ if (currentRun is null)
+ {
+ continue;
+ }
+
+ TextRun? nextRun = null;
+
+ if (index + 1 < TextRuns.Count)
+ {
+ nextRun = TextRuns[index + 1];
+ }
+
+ if (nextRun != null)
+ {
+ if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.End < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
+
+ goto noop;
+
+ skip:
+ {
+ startX += currentRun.Size.Width;
+ }
+
+ continue;
+
+ noop:
+ {
+ }
+ }
+
+
+ var endX = startX;
+ var endOffset = 0d;
+
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ endOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
+ shapedRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex + textLength) :
+ new CharacterHit(firstTextSourceCharacterIndex));
+
+ endX += endOffset;
+
+ var startOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
+ shapedRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex) :
+ new CharacterHit(firstTextSourceCharacterIndex + textLength));
+
+ startX += startOffset;
+
+ var characterHit = shapedRun.GlyphRun.IsLeftToRight ?
+ shapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
+ shapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ currentDirection = shapedRun.ShapedBuffer.IsLeftToRight ?
+ FlowDirection.LeftToRight :
+ FlowDirection.RightToLeft;
+
+ if (nextRun is ShapedTextCharacters nextShaped)
+ {
+ if (shapedRun.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight)
+ {
+ endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit(
+ nextShaped.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex + textLength) :
+ new CharacterHit(firstTextSourceCharacterIndex));
+
+ index++;
+
+ endX += endOffset;
+
+ currentRun = nextShaped;
+
+ if (nextShaped.ShapedBuffer.IsLeftToRight)
+ {
+ characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ }
+ }
+ }
+
+ break;
+ }
+ default:
+ {
+ if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
+ {
+ endX += currentRun.Size.Width;
+ }
+
+ break;
+ }
+ }
+
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
+ }
+
+ var width = endX - startX;
+
+ if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+ {
+ var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
+
+ result[result.Count - 1] = textBounds;
+ }
+ else
+ {
+ currentRect = new Rect(startX, 0, width, Height);
+
+ result.Add(new TextBounds(currentRect, currentDirection));
+ }
+
+ if (currentDirection == FlowDirection.LeftToRight)
+ {
+ if (nextRun != null)
+ {
+ if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
+ {
+ break;
+ }
+
+ currentPosition = nextRun.Text.End;
+ }
+ else
+ {
+ if (currentPosition >= firstTextSourceCharacterIndex + textLength)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ if (currentPosition <= firstTextSourceCharacterIndex)
+ {
+ break;
+ }
+
+ if (currentPosition != currentRun.Text.Start)
+ {
+ endX += currentRun.Size.Width - endOffset;
+ }
+ }
+
+ lastDirection = currentDirection;
+ startX = endX;
+ }
+
+ return result;
}
public TextLineImpl FinalizeLine()
@@ -303,18 +597,37 @@ namespace Avalonia.Media.TextFormatting
return this;
}
+ private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
+ {
+ if (run is ShapedTextCharacters shapedTextCharacters)
+ {
+ return shapedTextCharacters.BidiLevel;
+ }
+
+ var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
+
+ return (sbyte)defaultLevel;
+ }
+
private void BidiReorder()
{
+ if (_textRuns.Count == 0)
+ {
+ return;
+ }
+
// Build up the collection of ordered runs.
var run = _textRuns[0];
- OrderedBidiRun orderedRun = new(run);
+
+ OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _flowDirection));
+
var current = orderedRun;
for (var i = 1; i < _textRuns.Count; i++)
{
run = _textRuns[i];
- current.Next = new OrderedBidiRun(run);
+ current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _flowDirection));
current = current.Next;
}
@@ -331,7 +644,9 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < _textRuns.Count; i++)
{
- var level = _textRuns[i].BidiLevel;
+ var currentRun = _textRuns[i];
+
+ var level = GetRunBidiLevel(currentRun, _flowDirection);
if (level > max)
{
@@ -366,9 +681,9 @@ namespace Avalonia.Media.TextFormatting
{
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
- if (!current.Run.IsReversed)
+ if (current.Run is ShapedTextCharacters { IsReversed: false } shapedTextCharacters)
{
- current.Run.Reverse();
+ shapedTextCharacters.Reverse();
}
}
@@ -464,53 +779,73 @@ namespace Avalonia.Media.TextFormatting
nextCharacterHit = characterHit;
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ var lastCodepointIndex = FirstTextSourceIndex + Length;
- if (codepointIndex >= TextRange.End)
+ if (codepointIndex >= lastCodepointIndex)
{
return false; // Cannot go forward anymore
}
- if (codepointIndex < TextRange.Start)
+ if (codepointIndex < FirstTextSourceIndex)
{
- codepointIndex = TextRange.Start;
+ codepointIndex = FirstTextSourceIndex;
}
- var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward);
+ var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Count)
{
- var run = _textRuns[runIndex];
+ var currentRun = _textRuns[runIndex];
+
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
- var foundCharacterHit =
- run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength,
- out _);
+ var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length;
- var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
- TextRange.Start + TextRange.Length;
+ if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight)
+ {
+ nextCharacterHit = foundCharacterHit;
- if (isAtEnd && !run.GlyphRun.IsLeftToRight)
- {
- nextCharacterHit = foundCharacterHit;
+ return true;
+ }
- return true;
- }
+ var characterIndex = codepointIndex - shapedRun.Text.Start;
- var characterIndex = codepointIndex - run.Text.Start;
+ if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ }
- if (characterIndex < 0 && run.ShapedBuffer.IsLeftToRight)
- {
- foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
- }
+ nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
+ foundCharacterHit :
+ new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
+
+ if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
+ {
+ return true;
+ }
- nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
- foundCharacterHit :
- new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
+ break;
+ }
+ default:
+ {
+ var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
- {
- return true;
+ if (textPosition == currentPosition)
+ {
+ nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+
+ return true;
+ }
+
+ break;
+ }
}
+ currentPosition += currentRun.TextSourceLength;
runIndex++;
}
@@ -527,45 +862,67 @@ namespace Avalonia.Media.TextFormatting
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- if (characterIndex == TextRange.Start)
+ if (characterIndex == FirstTextSourceIndex)
{
- previousCharacterHit = new CharacterHit(TextRange.Start);
+ previousCharacterHit = new CharacterHit(FirstTextSourceIndex);
return true;
}
previousCharacterHit = characterHit;
- if (characterIndex < TextRange.Start)
+ if (characterIndex < FirstTextSourceIndex)
{
return false; // Cannot go backward anymore.
}
- var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward);
+ var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
while (runIndex >= 0)
{
- var run = _textRuns[runIndex];
-
- var foundCharacterHit =
- run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
+ var currentRun = _textRuns[runIndex];
- if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
+ switch (currentRun)
{
- previousCharacterHit = foundCharacterHit;
-
- return true;
- }
-
- previousCharacterHit = characterHit.TrailingLength != 0 ?
- foundCharacterHit :
- new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ case ShapedTextCharacters shapedRun:
+ {
+ var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
- if (previousCharacterHit != characterHit)
- {
- return true;
+ if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
+ {
+ previousCharacterHit = foundCharacterHit;
+
+ return true;
+ }
+
+ var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength;
+
+ if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex)
+ {
+ previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ }
+
+ if (previousCharacterHit != characterHit)
+ {
+ return true;
+ }
+
+ break;
+ }
+ default:
+ {
+ if (characterIndex == currentPosition + currentRun.TextSourceLength)
+ {
+ previousCharacterHit = new CharacterHit(currentPosition);
+
+ return true;
+ }
+
+ break;
+ }
}
+ currentPosition -= currentRun.TextSourceLength;
runIndex--;
}
@@ -577,59 +934,83 @@ namespace Avalonia.Media.TextFormatting
///
/// The codepoint index.
/// The logical direction.
+ /// The text position of the found run index.
/// The text run index.
- private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction)
+ private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
{
var runIndex = 0;
- ShapedTextCharacters? previousRun = null;
+ textPosition = FirstTextSourceIndex;
+ DrawableTextRun? previousRun = null;
while (runIndex < _textRuns.Count)
{
var currentRun = _textRuns[runIndex];
- if (previousRun != null && !previousRun.ShapedBuffer.IsLeftToRight)
+ switch (currentRun)
{
- if (currentRun.ShapedBuffer.IsLeftToRight)
- {
- if (currentRun.Text.Start >= codepointIndex)
+ case ShapedTextCharacters shapedRun:
{
- return --runIndex;
+ if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
+ {
+ if (shapedRun.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)
+ {
+ return runIndex;
+ }
+
+ break;
}
- }
- else
- {
- if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+
+ default:
{
- return --runIndex;
- }
- }
- }
+ if (codepointIndex == textPosition)
+ {
+ 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)
+ {
+ return runIndex;
+ }
- if (runIndex + 1 < _textRuns.Count)
- {
- runIndex++;
- previousRun = currentRun;
- }
- else
- {
- break;
+ break;
+ }
}
+
+ runIndex++;
+ previousRun = currentRun;
+ textPosition += currentRun.TextSourceLength;
}
return runIndex;
@@ -637,6 +1018,8 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics()
{
+ var start = 0d;
+ var height = 0d;
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
@@ -646,78 +1029,150 @@ namespace Avalonia.Media.TextFormatting
var lineGap = 0d;
var fontRenderingEmSize = 0d;
- for (var index = 0; index < _textRuns.Count; index++)
+ var lineHeight = _paragraphProperties.LineHeight;
+
+ if (_textRuns.Count == 0)
{
- var textRun = _textRuns[index];
+ var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
+ fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
+ var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
+ ascent = glyphTypeface.Ascent * scale;
+ height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
+ descent - ascent + lineGap :
+ lineHeight;
- var fontMetrics =
- new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+ return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0);
+ }
- if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
+ for (var index = 0; index < _textRuns.Count; index++)
+ {
+ switch (_textRuns[index])
{
- fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
-
- if (ascent > fontMetrics.Ascent)
- {
- ascent = fontMetrics.Ascent;
- }
-
- if (descent < fontMetrics.Descent)
- {
- descent = fontMetrics.Descent;
- }
+ case ShapedTextCharacters textRun:
+ {
+ var fontMetrics =
+ new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
- if (lineGap < fontMetrics.LineGap)
- {
- lineGap = fontMetrics.LineGap;
- }
- }
+ if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
+ {
+ 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;
+ }
+
+ if (descent - ascent + lineGap > height)
+ {
+ height = descent - ascent + lineGap;
+ }
+ }
- switch (_paragraphProperties.FlowDirection)
- {
- case FlowDirection.LeftToRight:
- {
- if (index == _textRuns.Count - 1)
+ switch (_paragraphProperties.FlowDirection)
{
- width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
- trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ 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];
+
+ if (firstRun is ShapedTextCharacters shapedTextCharacters)
+ {
+ var offset = shapedTextCharacters.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
+ shapedTextCharacters.GlyphRun.Metrics.Width;
+
+ width = widthIncludingWhitespace +
+ textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
+
+ trailingWhitespaceLength = shapedTextCharacters.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = shapedTextCharacters.GlyphRun.Metrics.NewlineLength;
+ }
+ }
+
+ break;
+ }
}
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
+
break;
}
- case FlowDirection.RightToLeft:
+ case { } drawableTextRun:
{
- if (index == _textRuns.Count - 1)
- {
- var firstRun = _textRuns[0];
+ widthIncludingWhitespace += drawableTextRun.Size.Width;
- var offset = firstRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
- firstRun.GlyphRun.Metrics.Width;
+ switch (_paragraphProperties.FlowDirection)
+ {
+ case FlowDirection.LeftToRight:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
+ newLineLength = 0;
+ }
+
+ break;
+ }
+
+ case FlowDirection.RightToLeft:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
+ newLineLength = 0;
+ }
+
+ break;
+ }
+ }
- width = widthIncludingWhitespace +
- textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
+ if (drawableTextRun.Size.Height > height)
+ {
+ height = drawableTextRun.Size.Height;
+ }
- trailingWhitespaceLength = firstRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = firstRun.GlyphRun.Metrics.NewlineLength;
+ if (ascent > -drawableTextRun.Baseline)
+ {
+ ascent = -drawableTextRun.Baseline;
}
break;
}
}
-
- widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
}
- var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
+ 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;
+ if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
+ {
+ height = lineHeight;
+ }
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
@@ -725,15 +1180,17 @@ namespace Avalonia.Media.TextFormatting
private sealed class OrderedBidiRun
{
- public OrderedBidiRun(ShapedTextCharacters run) => Run = run;
+ public OrderedBidiRun(DrawableTextRun run, sbyte level)
+ {
+ Run = run;
+ Level = level;
+ }
- public sbyte Level => Run.BidiLevel;
+ public sbyte Level { get; }
- public ShapedTextCharacters Run { get; }
+ public DrawableTextRun Run { get; }
public OrderedBidiRun? Next { get; set; }
-
- public void Reverse() => Run.ShapedBuffer.GlyphInfos.Span.Reverse();
}
private sealed class BidiRange
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
index b799567a60..82a0ba14d8 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
@@ -63,5 +63,13 @@
{
get { return 0; }
}
+
+ ///
+ /// Default Incremental Tab
+ ///
+ public virtual double DefaultIncrementalTab
+ {
+ get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
index 99fcbd805f..86b701cb4b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Run vertical box alignment
///
- public abstract BaselineAlignment BaselineAlignment { get; }
+ public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
public bool Equals(TextRunProperties? other)
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
index c982a435c3..615b1553b6 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
@@ -45,10 +45,9 @@ namespace Avalonia.Media.TextFormatting
}
///
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo? culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
- return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture, bidiLevel);
+ return _platformImpl.ShapeText(text, options);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs
new file mode 100644
index 0000000000..a7fe92dc9a
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs
@@ -0,0 +1,49 @@
+using System.Globalization;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// Options to customize text shaping.
+ ///
+ public readonly struct TextShaperOptions
+ {
+ public TextShaperOptions(
+ GlyphTypeface typeface,
+ double fontRenderingEmSize = 12,
+ sbyte bidiLevel = 0,
+ CultureInfo? culture = null,
+ double incrementalTabWidth = 0)
+ {
+ Typeface = typeface;
+ FontRenderingEmSize = fontRenderingEmSize;
+ BidLevel = bidiLevel;
+ Culture = culture;
+ IncrementalTabWidth = incrementalTabWidth;
+ }
+
+ ///
+ /// Get the typeface.
+ ///
+ public GlyphTypeface Typeface { get; }
+ ///
+ /// Get the font rendering em size.
+ ///
+ public double FontRenderingEmSize { get; }
+
+ ///
+ /// Get the bidi level of the text.
+ ///
+ public sbyte BidLevel { get; }
+
+ ///
+ /// Get the culture.
+ ///
+ public CultureInfo? Culture { get; }
+
+ ///
+ /// Get the incremental tab width.
+ ///
+ public double IncrementalTabWidth { get; }
+
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
index 83acaa021e..670d94e928 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Text used as collapsing symbol.
/// Width in which collapsing is constrained to.
- /// Text run properties of ellispis symbol.
+ /// Text run properties of ellipsis symbol.
public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties)
{
Width = width;
@@ -22,12 +22,12 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, false);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
index ff2e4cf325..dbffbdf060 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Text used as collapsing symbol.
/// width in which collapsing is constrained to.
- /// text run properties of ellispis symbol.
+ /// text run properties of ellipsis symbol.
public TextTrailingWordEllipsis(
ReadOnlySlice ellipsis,
double width,
@@ -26,12 +26,12 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, true);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
index 404956d1e1..d18a4b2a87 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
@@ -302,7 +302,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
///
/// The data to be evaluated
/// The resolved embedding level
- public sbyte ResolveEmbeddingLevel(ReadOnlySlice data)
+ public sbyte ResolveEmbeddingLevel(ArraySlice data)
{
// P2
for (var i = 0; i < data.Length; ++i)
diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
index aced05c9d8..11be9e3f09 100644
--- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
+++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
@@ -1,6 +1,4 @@
-using System.Globalization;
-using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Platform
@@ -14,11 +12,8 @@ namespace Avalonia.Platform
/// 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.
+ /// Text shaper options to customize the shaping process.
/// A shaped glyph run.
- ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, sbyte bidiLevel);
- }
+ ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options);
+ }
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
index 504256b932..c1fc6a81f6 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
@@ -59,6 +59,7 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context)
{
+ context.Transform = Transform;
context.DrawEllipse(Brush, Pen, Rect);
}
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index c4d11f4613..a0890262e7 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -13,12 +13,16 @@ namespace Avalonia.Skia
{
internal class TextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+ var culture = options.Culture;
+
using (var buffer = new Buffer())
{
- buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
+ buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
MergeBreakPair(buffer);
@@ -61,6 +65,15 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
+ if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ {
+ glyphIndex = typeface.GetGlyph(' ');
+
+ glyphAdvance = options.IncrementalTabWidth > 0 ?
+ options.IncrementalTabWidth :
+ 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
+ }
+
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index 62cf031f86..59027a663f 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -14,9 +14,13 @@ namespace Avalonia.Direct2D1.Media
internal class TextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+ var culture = options.Culture;
+
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
@@ -62,6 +66,15 @@ internal class TextShaperImpl : ITextShaperImpl
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
+ if (glyphIndex == 0 && text[glyphCluster] == '\t')
+ {
+ glyphIndex = typeface.GetGlyph(' ');
+
+ glyphAdvance = options.IncrementalTabWidth > 0 ?
+ options.IncrementalTabWidth :
+ 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
+ }
+
var targetInfo =
new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance,
glyphOffset);
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index c0c9e841f4..f15da8e0c5 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -865,6 +865,35 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void Should_Fullfill_MaxLines_Contraint()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ Text = "ABC",
+ MaxLines = 1,
+ AcceptsReturn= true
+ };
+
+ target.Measure(Size.Infinity);
+
+ AvaloniaLocator.CurrentMutable.Bind().ToSingleton();
+
+ var clipboard = AvaloniaLocator.CurrentMutable.GetService();
+ clipboard.SetTextAsync(Environment.NewLine).GetAwaiter().GetResult();
+
+ RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
+ clipboard.ClearAsync().GetAwaiter().GetResult();
+
+ RaiseTextEvent(target, Environment.NewLine);
+
+ Assert.Equal("ABC", target.Text);
+ }
+ }
+
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
diff --git a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json
index 5477bcfd29..eb57cfb8da 100644
--- a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json
+++ b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json
@@ -389,9 +389,9 @@
"dev": true
},
"ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
+ "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
"dev": true
},
"ansi-styles": {
@@ -587,9 +587,9 @@
},
"dependencies": {
"ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
},
"string-width": {
@@ -1333,9 +1333,9 @@
}
},
"minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"mocha": {
@@ -1437,9 +1437,9 @@
},
"dependencies": {
"ansi-regex": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
- "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
},
"cliui": {
@@ -2117,9 +2117,9 @@
},
"dependencies": {
"ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
},
"ansi-styles": {
@@ -2211,9 +2211,9 @@
},
"dependencies": {
"ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
},
"find-up": {
@@ -2305,9 +2305,9 @@
},
"dependencies": {
"ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
},
"find-up": {
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index e51fff5416..f12785a7ce 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -16,6 +16,7 @@ using System.ComponentModel;
using System.Linq;
using System.Xml;
using Xunit;
+using Avalonia.Controls.Documents;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
@@ -47,12 +48,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public void Attached_Property_Is_Set()
{
var xaml =
- @"";
+ @"";
var target = AvaloniaRuntimeXamlLoader.Parse(xaml);
Assert.NotNull(target);
- Assert.Equal(21.0, TextBlock.GetFontSize(target));
+ Assert.Equal(21.0, TextElement.GetFontSize(target));
}
[Fact]
@@ -90,13 +91,13 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
{
var xaml =
- @"";
+ @"";
var target = AvaloniaRuntimeXamlLoader.Parse(xaml);
target.DataContext = 21.0;
- Assert.Equal(21.0, TextBlock.GetFontSize(target));
+ Assert.Equal(21.0, TextElement.GetFontSize(target));
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
index 904f0935c4..3b9caa393e 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
@@ -17,8 +17,9 @@ namespace Avalonia.Skia.UnitTests.Media
{
using (Start())
{
+ var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
- TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default.GlyphTypeface, 10, CultureInfo.CurrentCulture, direction);
+ TextShaper.Current.ShapeText(text.AsMemory(), options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@@ -59,8 +60,9 @@ namespace Avalonia.Skia.UnitTests.Media
{
using (Start())
{
+ var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
- TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default.GlyphTypeface, 10, CultureInfo.CurrentCulture, direction);
+ TextShaper.Current.ShapeText(text.AsMemory(), options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@@ -103,8 +105,9 @@ namespace Avalonia.Skia.UnitTests.Media
{
using (Start())
{
+ var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
- TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default.GlyphTypeface, 10, CultureInfo.CurrentCulture, direction);
+ TextShaper.Current.ShapeText(text.AsMemory(), options);
var glyphRun = CreateGlyphRun(shapedBuffer);
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
index 2a20fdd9fe..005bcdf70e 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
@@ -20,15 +20,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
- if (textSourceIndex > 50)
+ if (textSourceIndex >= 50)
{
return null;
}
-
- if (textSourceIndex == 50)
- {
- return new TextEndOfParagraph();
- }
var index = textSourceIndex / 10;
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
index 65c342b065..dee4fe7f77 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
@@ -23,13 +23,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
var runText = _text.Skip(textSourceIndex);
-
- if (runText.IsEmpty)
- {
- return new TextEndOfParagraph();
- }
- return new TextCharacters(runText, _defaultGenericPropertiesRunProperties);
+ return runText.IsEmpty ? null : new TextCharacters(runText, _defaultGenericPropertiesRunProperties);
}
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 7c7fb4783e..9d40898608 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -58,7 +58,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(5, textLine.TextRuns.Count);
- Assert.Equal(50, textLine.TextRange.Length);
+ Assert.Equal(50, textLine.Length);
}
}
@@ -89,7 +89,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
- Assert.Equal(text.Length, textLine.TextRange.Length);
+ Assert.Equal(text.Length, textLine.Length);
for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++)
{
@@ -195,10 +195,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (text.Length - currentPosition > expectedCharactersPerLine)
{
- Assert.Equal(expectedCharactersPerLine, textLine.TextRange.Length);
+ Assert.Equal(expectedCharactersPerLine, textLine.Length);
}
- currentPosition += textLine.TextRange.Length;
+ currentPosition += textLine.Length;
numberOfLines++;
}
@@ -249,16 +249,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, currentPosition, paragraphWidth,
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
- Assert.True(expected.Contains(textLine.TextRange.End));
+ var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
- var index = expected.IndexOf(textLine.TextRange.End);
+ Assert.True(expected.Contains(end));
+
+ var index = expected.IndexOf(end);
for (var i = 0; i <= index; i++)
{
expected.RemoveAt(0);
}
- currentPosition += textLine.TextRange.Length;
+ currentPosition += textLine.Length;
}
}
}
@@ -312,7 +314,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(textLine.Width <= 200);
- textSourceIndex += textLine.TextRange.Length;
+ textSourceIndex += textLine.Length;
}
}
}
@@ -336,9 +338,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, textSourceIndex, 3, paragraphProperties);
- Assert.NotEqual(0, textLine.TextRange.Length);
+ Assert.NotEqual(0, textLine.Length);
- textSourceIndex += textLine.TextRange.Length;
+ textSourceIndex += textLine.Length;
}
Assert.Equal(text.Length, textSourceIndex);
@@ -383,7 +385,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, currentPosition, 300,
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
- currentPosition += textLine.TextRange.Length;
+ currentPosition += textLine.Length;
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
@@ -392,7 +394,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
currentHeight += textLine.Height;
- var currentText = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
+ var currentText = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
Assert.Equal(expectedLines[currentLineIndex], currentText);
@@ -485,9 +487,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
- Assert.Equal(textLine.TextRange.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
+ Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
- textPosition += textLine.TextRange.Length;
+ textPosition += textLine.Length;
lastBreak = textLine.TextLineBreak;
}
@@ -508,7 +510,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 33, paragraphProperties);
- Assert.NotNull(textLine.TextLineBreak?.RemainingCharacters);
+ Assert.NotNull(textLine.TextLineBreak?.RemainingRuns);
}
}
@@ -562,6 +564,98 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [Fact]
+ public void Should_FormatLine_With_DrawableRuns()
+ {
+ var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
+ var textSource = new CustomTextSource("Hello World ->");
+
+ using (Start())
+ {
+ var textLine =
+ TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
+
+ Assert.Equal(3, textLine.TextRuns.Count);
+
+ Assert.True(textLine.TextRuns[1] is RectangleRun);
+ }
+ }
+
+ [Fact]
+ public void Should_Format_With_EndOfLineRun()
+ {
+ using (Start())
+ {
+ var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
+ var textSource = new EndOfLineTextSource();
+
+ var textLine =
+ TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
+
+ Assert.NotNull(textLine.TextLineBreak);
+
+ Assert.Equal(TextRun.DefaultTextSourceLength, textLine.Length);
+ }
+ }
+
+ private class EndOfLineTextSource : ITextSource
+ {
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ return new TextEndOfLine();
+ }
+ }
+
+ private class CustomTextSource : ITextSource
+ {
+ private readonly string _text;
+
+ public CustomTextSource(string text)
+ {
+ _text = text;
+ }
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ if (textSourceIndex >= _text.Length + TextRun.DefaultTextSourceLength + _text.Length)
+ {
+ return null;
+ }
+
+ if (textSourceIndex == _text.Length)
+ {
+ return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green);
+ }
+
+ return new TextCharacters(_text.AsMemory(),
+ new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
+ }
+ }
+
+ private class RectangleRun : DrawableTextRun
+ {
+ private readonly Rect _rect;
+ private readonly IBrush _fill;
+
+ public RectangleRun(Rect rect, IBrush fill)
+ {
+ _rect = rect;
+ _fill = fill;
+ }
+
+ public override Size Size => _rect.Size;
+ public override double Baseline => 0;
+ public override void Draw(DrawingContext drawingContext, Point origin)
+ {
+ using (drawingContext.PushPreTransform(Matrix.CreateTranslation(new Vector(origin.X, 0))))
+ {
+ drawingContext.FillRectangle(_fill, _rect);
+ }
+ }
+ }
+
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index d331def414..7e1103d624 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -88,8 +88,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textWrapping: TextWrapping.Wrap,
maxWidth: 200);
- var expectedLines = expected.TextLines.Select(x => text.Substring(x.TextRange.Start,
- x.TextRange.Length)).ToList();
+ var expectedLines = expected.TextLines.Select(x => text.Substring(x.FirstTextSourceIndex,
+ x.Length)).ToList();
var spans = new[]
{
@@ -106,8 +106,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
maxWidth: 200,
textStyleOverrides: spans);
- var actualLines = actual.TextLines.Select(x => text.Substring(x.TextRange.Start,
- x.TextRange.Length)).ToList();
+ var actualLines = actual.TextLines.Select(x => text.Substring(x.FirstTextSourceIndex,
+ x.Length)).ToList();
Assert.Equal(expectedLines.Count, actualLines.Count);
@@ -140,7 +140,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
black,
textWrapping: TextWrapping.Wrap);
- var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
+ var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast()
+ .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
var outer = new GraphemeEnumerator(text.AsMemory());
var inner = new GraphemeEnumerator(text.AsMemory());
@@ -172,7 +173,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textWrapping: TextWrapping.Wrap,
textStyleOverrides: spans);
- var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
+ var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast()
+ .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);
@@ -348,7 +350,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
12.0f,
Brushes.Black.ToImmutable());
- Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
+ Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.Length));
}
}
@@ -813,7 +815,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(textLine.Width <= maxWidth);
var actual = new string(textLine.TextRuns.Cast().OrderBy(x => x.Text.Start).SelectMany(x => x.Text).ToArray());
- var expected = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
+ var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
Assert.Equal(expected, actual);
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 367e6f4bea..e9bc792be3 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -35,9 +35,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var firstCharacterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(int.MinValue));
- Assert.Equal(textLine.TextRange.Start, firstCharacterHit.FirstCharacterIndex);
+ Assert.Equal(textLine.FirstTextSourceIndex, firstCharacterHit.FirstCharacterIndex);
- currentIndex += textLine.TextRange.Length;
+ currentIndex += textLine.Length;
}
}
}
@@ -63,10 +63,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var lastCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(int.MaxValue));
- Assert.Equal(textLine.TextRange.Start + textLine.TextRange.Length,
+ Assert.Equal(textLine.FirstTextSourceIndex + textLine.Length,
lastCharacterHit.FirstCharacterIndex + lastCharacterHit.TrailingLength);
- currentIndex += textLine.TextRange.Length;
+ currentIndex += textLine.Length;
}
}
}
@@ -277,6 +277,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
firstCharacterHit = previousCharacterHit;
+ firstCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
+
previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex);
@@ -416,24 +418,160 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[Fact]
- public void TextLineBreak_Should_Contain_TextEndOfLine()
+ public void Should_Get_Next_CharacterHit_For_Drawable_Runs()
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var textSource = new DrawableRunTextSource();
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ Assert.Equal(4, textLine.TextRuns.Count);
+
+ var currentHit = textLine.GetNextCaretCharacterHit(new CharacterHit(0));
+
+ Assert.Equal(1, currentHit.FirstCharacterIndex);
+ Assert.Equal(0, currentHit.TrailingLength);
+
+ currentHit = textLine.GetNextCaretCharacterHit(currentHit);
+
+ Assert.Equal(2, currentHit.FirstCharacterIndex);
+ Assert.Equal(0, currentHit.TrailingLength);
+
+ currentHit = textLine.GetNextCaretCharacterHit(currentHit);
+
+ Assert.Equal(3, currentHit.FirstCharacterIndex);
+ Assert.Equal(0, currentHit.TrailingLength);
+
+ currentHit = textLine.GetNextCaretCharacterHit(currentHit);
+
+ Assert.Equal(3, currentHit.FirstCharacterIndex);
+ Assert.Equal(1, currentHit.TrailingLength);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Previous_CharacterHit_For_Drawable_Runs()
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var textSource = new DrawableRunTextSource();
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ Assert.Equal(4, textLine.TextRuns.Count);
+
+ var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1));
+
+ Assert.Equal(3, currentHit.FirstCharacterIndex);
+ Assert.Equal(0, currentHit.TrailingLength);
+
+ currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
+
+ Assert.Equal(2, currentHit.FirstCharacterIndex);
+ Assert.Equal(0, currentHit.TrailingLength);
+
+ currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
+
+ Assert.Equal(1, currentHit.FirstCharacterIndex);
+ Assert.Equal(0, currentHit.TrailingLength);
+
+ currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
+
+ Assert.Equal(0, currentHit.FirstCharacterIndex);
+ Assert.Equal(0, currentHit.TrailingLength);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_CharacterHit_From_Distance_For_Drawable_Runs()
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var textSource = new DrawableRunTextSource();
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ var characterHit = textLine.GetCharacterHitFromDistance(50);
+
+ Assert.Equal(3, characterHit.FirstCharacterIndex);
+ Assert.Equal(1, characterHit.TrailingLength);
+
+ characterHit = textLine.GetCharacterHitFromDistance(32);
+
+ Assert.Equal(2, characterHit.FirstCharacterIndex);
+ Assert.Equal(1, characterHit.TrailingLength);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Distance_From_CharacterHit_Drawable_Runs()
{
using (Start())
{
- var defaultTextRunProperties =
- new GenericTextRunProperties(Typeface.Default);
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var textSource = new DrawableRunTextSource();
- const string text = "0123456789";
+ var formatter = new TextFormatterImpl();
- var source = new SingleBufferTextSource(text, defaultTextRunProperties);
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
- var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties);
+ var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(1));
- var formatter = TextFormatter.Current;
+ Assert.Equal(14, distance);
- var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
+ distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(2));
- Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
+ Assert.True(distance > 14);
+ }
+ }
+
+ private class DrawableRunTextSource : ITextSource
+ {
+ const string Text = "_A_A";
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ switch (textSourceIndex)
+ {
+ case 0:
+ return new CustomDrawableRun();
+ case 1:
+ return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
+ case 2:
+ return new CustomDrawableRun();
+ case 3:
+ return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
+ default:
+ return null;
+ }
+ }
+ }
+
+ private class CustomDrawableRun : DrawableTextRun
+ {
+ public override Size Size => new(14, 14);
+ public override double Baseline => 14;
+ public override void Draw(DrawingContext drawingContext, Point origin)
+ {
+
}
}
@@ -517,6 +655,63 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return rects;
}
+ [Fact]
+ public void Should_Get_TextBounds()
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var text = "0123".AsMemory();
+ var ltrOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
+ var rtlOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 1, CultureInfo.CurrentCulture);
+
+ var textRuns = new List
+ {
+ new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), ltrOptions), defaultProperties),
+ new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length, text.Length), ltrOptions), defaultProperties),
+ new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2, text.Length), rtlOptions), defaultProperties),
+ new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
+ };
+
+
+ var textSource = new FixedRunsTextSource(textRuns);
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ var textBounds = textLine.GetTextBounds(0, text.Length * 4);
+
+ Assert.Equal(3, textBounds.Count);
+ Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
+ }
+ }
+
+ private class FixedRunsTextSource : ITextSource
+ {
+ private readonly IReadOnlyList _textRuns;
+
+ public FixedRunsTextSource(IReadOnlyList textRuns)
+ {
+ _textRuns = textRuns;
+ }
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ foreach (var textRun in _textRuns)
+ {
+ if(textRun.Text.Start == textSourceIndex)
+ {
+ return textRun;
+ }
+ }
+
+ return null;
+ }
+ }
+
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
index 57676ad581..94933e334d 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
@@ -15,12 +15,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var text = "\n\r\n".AsMemory();
-
- var shapedBuffer = TextShaper.Current.ShapeText(
- text,
- Typeface.Default.GlyphTypeface,
- 12,
- CultureInfo.CurrentCulture, 0);
+ var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture);
+ var shapedBuffer = TextShaper.Current.ShapeText(text, options);
Assert.Equal(shapedBuffer.Text.Length, text.Length);
Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length);
@@ -29,7 +25,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, shapedBuffer.GlyphClusters[2]);
}
}
-
+
+ [Fact]
+ public void Should_Apply_IncrementalTabWidth()
+ {
+ using (Start())
+ {
+ var text = "\t".AsMemory();
+ var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100);
+ var shapedBuffer = TextShaper.Current.ShapeText(text, options);
+
+ Assert.Equal(shapedBuffer.Length, text.Length);
+ Assert.Equal(100, shapedBuffer.GlyphAdvances[0]);
+ }
+ }
+
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
index 8ad3284490..5f8854b3ab 100644
--- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
@@ -1,6 +1,5 @@
using System;
using System.Globalization;
-using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
@@ -12,9 +11,13 @@ namespace Avalonia.UnitTests
{
public class HarfBuzzTextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+ var culture = options.Culture;
+
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
index fc22791102..c4b1e6c154 100644
--- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
@@ -1,6 +1,4 @@
-using System.Globalization;
-using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@@ -9,9 +7,12 @@ namespace Avalonia.UnitTests
{
public class MockTextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
for (var i = 0; i < shapedBuffer.Length;)