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;)