From 9250d9340723554b5a05726f2dffc003c26888f3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 29 Mar 2022 09:44:25 +0200 Subject: [PATCH 01/17] Implement embedded runs Improve text edit navigation Implement IncrementalTabWidth --- samples/ControlCatalog/Pages/PointersPage.cs | 3 +- samples/RenderDemo/MainWindow.xaml | 3 + .../RenderDemo/Pages/TextFormatterPage.axaml | 7 + .../Pages/TextFormatterPage.axaml.cs | 118 +++ samples/RenderDemo/RenderDemo.csproj | 4 + .../HamburgerMenu/HamburgerMenu.xaml | 18 +- .../DataGridTextColumn.cs | 3 +- .../Themes/Default.xaml | 2 +- .../Themes/Fluent.xaml | 2 +- src/Avalonia.Controls/ApiCompatBaseline.txt | 22 +- src/Avalonia.Controls/Control.cs | 114 ++- .../Documents/TextElement.cs | 167 +++- .../Presenters/TextPresenter.cs | 89 +- .../Primitives/TemplatedControl.cs | 75 -- src/Avalonia.Controls/TextBlock.cs | 240 +---- src/Avalonia.Controls/TextBox.cs | 109 ++- src/Avalonia.Controls/Window.cs | 1 - .../Diagnostics/Views/ConsoleView.xaml | 4 +- .../Views/LayoutExplorerView.axaml | 2 +- .../HeadlessPlatformStubs.cs | 7 +- .../Controls/Button.xaml | 4 +- .../Controls/CaptionButtons.xaml | 4 +- .../Controls/CheckBox.xaml | 4 +- .../Controls/DatePicker.xaml | 22 +- .../Controls/RepeatButton.xaml | 4 +- .../Controls/SplitButton.xaml | 46 +- .../Controls/TimePicker.xaml | 16 +- .../Controls/ToggleButton.xaml | 2 +- .../Controls/Button.xaml | 16 +- .../Controls/CalendarItem.xaml | 6 +- .../Controls/CaptionButtons.xaml | 2 +- .../Controls/CheckBox.xaml | 24 +- .../Controls/ComboBox.xaml | 28 +- .../Controls/ComboBoxItem.xaml | 18 +- .../Controls/DataValidationErrors.xaml | 2 +- .../Controls/DatePicker.xaml | 22 +- .../Controls/Expander.xaml | 2 +- .../Controls/ListBox.xaml | 2 +- .../Controls/ListBoxItem.xaml | 14 +- .../Controls/MenuItem.xaml | 16 +- .../Controls/RadioButton.xaml | 10 +- .../Controls/RepeatButton.xaml | 8 +- .../Controls/Slider.xaml | 12 +- .../Controls/SplitButton.xaml | 46 +- .../Controls/TabItem.xaml | 20 +- .../Controls/TabStripItem.xaml | 20 +- .../Controls/TimePicker.xaml | 16 +- .../Controls/ToggleButton.xaml | 24 +- .../Controls/TreeViewItem.xaml | 16 +- .../Controls/WindowNotificationManager.xaml | 2 +- src/Avalonia.Visuals/ApiCompatBaseline.txt | 12 +- src/Avalonia.Visuals/Media/FormattedText.cs | 4 +- .../Media/TextDecorationCollection.cs | 10 + .../Media/TextFormatting/DrawableTextRun.cs | 5 + .../TextFormatting/FormattedTextSource.cs | 2 +- .../TextFormatting/ShapedTextCharacters.cs | 2 + .../Media/TextFormatting/TextBounds.cs | 29 + .../Media/TextFormatting/TextCharacters.cs | 25 +- .../TextCollapsingProperties.cs | 2 +- .../TextFormatting/TextEllipsisHelper.cs | 112 ++- .../Media/TextFormatting/TextFormatterImpl.cs | 243 +++-- .../Media/TextFormatting/TextLayout.cs | 175 +--- .../TextLeadingPrefixCharacterEllipsis.cs | 127 +-- .../Media/TextFormatting/TextLine.cs | 8 + .../Media/TextFormatting/TextLineBreak.cs | 8 +- .../Media/TextFormatting/TextLineImpl.cs | 896 +++++++++++++----- .../TextFormatting/TextParagraphProperties.cs | 8 + .../Media/TextFormatting/TextRunProperties.cs | 2 +- .../Media/TextFormatting/TextShaper.cs | 5 +- .../Media/TextFormatting/TextShaperOptions.cs | 49 + .../TextTrailingCharacterEllipsis.cs | 8 +- .../TextTrailingWordEllipsis.cs | 8 +- .../Platform/ITextShaperImpl.cs | 13 +- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 19 +- .../Media/TextShaperImpl.cs | 17 +- .../TextBoxTests.cs | 29 + .../Xaml/BasicTests.cs | 9 +- .../Media/GlyphRunTests.cs | 9 +- .../TextFormatting/MultiBufferTextSource.cs | 7 +- .../TextFormatting/SingleBufferTextSource.cs | 7 +- .../TextFormatting/TextFormatterTests.cs | 94 +- .../Media/TextFormatting/TextLineTests.cs | 197 +++- .../Media/TextFormatting/TextShaperTests.cs | 24 +- .../HarfBuzzTextShaperImpl.cs | 9 +- .../Avalonia.UnitTests/MockTextShaperImpl.cs | 11 +- 85 files changed, 2332 insertions(+), 1271 deletions(-) create mode 100644 samples/RenderDemo/Pages/TextFormatterPage.axaml create mode 100644 samples/RenderDemo/Pages/TextFormatterPage.axaml.cs create mode 100644 src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs create mode 100644 src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs 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}" /> @@ -112,7 +112,7 @@ diff --git a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml index a9a03c8ed5..b3b5fe7859 100644 --- a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml @@ -6,7 +6,7 @@ Value="{DynamicResource ThemeBorderLowBrush}" /> - @@ -24,7 +24,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" - TextBlock.Foreground="{TemplateBinding Foreground}" + 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..9af1c073cd 100644 --- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml @@ -56,7 +56,7 @@ @@ -157,10 +157,10 @@ SplitButton /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter"> - + @@ -169,10 +169,10 @@ SplitButton:pressed /template/ Border#SeparatorBorder"> - + @@ -181,10 +181,10 @@ SplitButton:flyout-open /template/ Border#SeparatorBorder"> - + @@ -193,10 +193,10 @@ SplitButton:disabled /template/ Border#SeparatorBorder"> - + @@ -205,10 +205,10 @@ SplitButton:checked /template/ Border#SeparatorBorder"> - + @@ -216,10 +216,10 @@ SplitButton:checked /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter"> - + @@ -227,10 +227,10 @@ SplitButton:checked /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter"> - + @@ -239,10 +239,10 @@ SplitButton:pressed:checked /template/ Border#SeparatorBorder"> - + @@ -251,10 +251,10 @@ SplitButton:checked:flyout-open /template/ Border#SeparatorBorder"> - + @@ -263,9 +263,9 @@ SplitButton:checked:disabled /template/ Border#SeparatorBorder"> - + diff --git a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml index a58fd62a99..5a6eb15f2d 100644 --- a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml @@ -33,7 +33,7 @@ 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 @@ @@ -177,19 +177,19 @@ @@ -199,28 +199,28 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml index a958d785fa..80c1103ece 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml @@ -18,14 +18,14 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index 12e148d2f9..05f5decbcf 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -29,7 +29,7 @@ @@ -125,7 +125,7 @@ @@ -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..efc6205ab1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -53,7 +53,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml index 5ee2dc527f..81aa5008cc 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml @@ -38,14 +38,14 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index e0dd0180b3..8840397843 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -32,7 +32,7 @@ @@ -133,10 +133,10 @@ SplitButton /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter"> - + @@ -145,10 +145,10 @@ SplitButton:pressed /template/ Border#SeparatorBorder"> - + @@ -157,10 +157,10 @@ SplitButton:flyout-open /template/ Border#SeparatorBorder"> - + @@ -169,10 +169,10 @@ SplitButton:disabled /template/ Border#SeparatorBorder"> - + @@ -181,10 +181,10 @@ SplitButton:checked /template/ Border#SeparatorBorder"> - + @@ -192,10 +192,10 @@ SplitButton:checked /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter"> - + @@ -203,10 +203,10 @@ SplitButton:checked /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter"> - + @@ -215,10 +215,10 @@ SplitButton:pressed:checked /template/ Border#SeparatorBorder"> - + @@ -227,10 +227,10 @@ SplitButton:checked:flyout-open /template/ Border#SeparatorBorder"> - + @@ -239,9 +239,9 @@ SplitButton:checked:disabled /template/ Border#SeparatorBorder"> - + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml index 83411b425b..20039fd71b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml @@ -19,7 +19,7 @@ - + @@ -37,9 +37,9 @@ Content="{TemplateBinding Header}" 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}" /> @@ -61,7 +61,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml index 694e9ef579..08fa944107 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml @@ -18,7 +18,7 @@ - + @@ -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..e39d6c4a73 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -34,7 +34,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index e66392113f..4cabe1e9d3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -58,7 +58,7 @@ - + @@ -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..c0905a0929 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. @@ -108,6 +110,7 @@ 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.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. 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. @@ -120,6 +123,7 @@ CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormat 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 +134,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 +173,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: 179 diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index 1cac3243e3..ed17824493 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;) 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..9940f2f3f8 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs @@ -30,7 +30,7 @@ namespace Avalonia.Media.TextFormatting if (runText.IsEmpty) { - return new TextEndOfParagraph(); + return null; } var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier); 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..5d0bb442d7 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; } @@ -22,69 +20,103 @@ namespace Avalonia.Media.TextFormatting 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 < textRange.End) + { + 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; + + var collapsedRuns = new List(textRuns.Count); - currentBreakPosition = nextBreakPosition; + if (collapsedLength > 0) + { + var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + + collapsedRuns.AddRange(splitResult.First); + + TextLineImpl.SortRuns(collapsedRuns); } - 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); + TextLineImpl.SortRuns(collapsedRuns); + } - return shapedTextCharacters; - } + collapsedRuns.Add(shapedSymbol); - availableWidth -= currentRun.Size.Width; + return collapsedRuns; + } + + break; + } + } - 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..fddd7c0160 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,20 +15,20 @@ 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); - 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) { @@ -44,9 +42,9 @@ namespace Avalonia.Media.TextFormatting { case TextWrapping.NoWrap: { - TextLineImpl.SortRuns(shapedRuns); + TextLineImpl.SortRuns(drawableTextRuns); - textLine = new TextLineImpl(shapedRuns, textRange, paragraphWidth, paragraphProperties, + textLine = new TextLineImpl(drawableTextRuns, textRange, paragraphWidth, paragraphProperties, flowDirection, nextLineBreak); textLine.FinalizeLine(); @@ -56,7 +54,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { - textLine = PerformTextWrapping(shapedRuns, textRange, paragraphWidth, paragraphProperties, + textLine = PerformTextWrapping(drawableTextRuns, textRange, paragraphWidth, paragraphProperties, flowDirection, nextLineBreak); break; } @@ -73,7 +71,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 +81,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 +102,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 +116,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); - var split = currentRun.Split(length - currentLength); + if (currentRun is not ShapedTextCharacters shapedTextCharacters) + { + throw new NotSupportedException("Only shaped runs can be split in between."); + } + + var split = shapedTextCharacters.Split(length - currentLength); first.Add(split.First); @@ -136,32 +140,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 +188,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 currentRun = processedRuns[index]; + + switch (currentRun) { - var nextRun = shapeableRuns[index + 1]; + case DrawableTextRun drawableRun: + { + drawableTextRuns.Add(drawableRun); - if (currentRun.CanShapeTogether(nextRun)) + break; + } + + case ShapeableTextCharacters shapeableRun: { - groupedRuns.Add(nextRun); + 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; - length += nextRun.Text.Length; - - if (start > nextRun.Text.Start) + while (index + 1 < processedRuns.Count) { - start = nextRun.Text.Start; - } + if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun) + { + break; + } - if (bufferOffset > nextRun.Text.BufferOffset) - { - bufferOffset = nextRun.Text.BufferOffset; + if (shapeableRun.CanShapeTogether(nextRun)) + { + groupedRuns.Add(nextRun); + + length += nextRun.Text.Length; + + 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); + + index++; + + shapeableRun = nextRun; + + continue; + } + + break; } - text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset); - - index++; + var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, + currentRun.Properties.FontRenderingEmSize, + shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab); - currentRun = nextRun; + drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); - continue; + break; } - - break; } - - shapedTextCharacters.AddRange(ShapeTogether(groupedRuns, text)); } - 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 +293,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 +312,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;) @@ -334,14 +383,14 @@ namespace Avalonia.Media.TextFormatting /// /// The formatted text runs. /// - private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, + private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextEndOfLine? endOfLine, out TextRange textRange) { var length = 0; endOfLine = null; - var textRuns = new List(); + var textRuns = new List(); var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); @@ -354,6 +403,17 @@ namespace Avalonia.Media.TextFormatting break; } + if (textRun is TextEndOfLine textEndOfLine) + { + endOfLine = textEndOfLine; + + textRuns.Add(textRun); + + length += textRun.TextSourceLength; + + break; + } + switch (textRun) { case TextCharacters textCharacters: @@ -376,12 +436,14 @@ namespace Avalonia.Media.TextFormatting break; } - case TextEndOfLine textEndOfLine: - endOfLine = textEndOfLine; + case DrawableTextRun drawableTextRun: + { + textRuns.Add(drawableTextRun); break; + } } - length += textRun.Text.Length; + length += textRun.TextSourceLength; } textRange = new TextRange(firstTextSourceIndex, length); @@ -415,7 +477,7 @@ namespace Avalonia.Media.TextFormatting return false; } - private static int MeasureLength(IReadOnlyList textRuns, TextRange textRange, + private static int MeasureLength(IReadOnlyList textRuns, TextRange textRange, double paragraphWidth) { var currentWidth = 0.0; @@ -423,19 +485,42 @@ namespace Avalonia.Media.TextFormatting foreach (var currentRun in textRuns) { - for (var i = 0; i < currentRun.ShapedBuffer.Length; i++) + switch (currentRun) { - var glyphInfo = currentRun.ShapedBuffer[i]; - - if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) + case ShapedTextCharacters shapedTextCharacters: { - var measuredLength = lastCluster - textRange.Start; + for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) + { + var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; - return measuredLength == 0 ? 1 : measuredLength; + if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) + { + var measuredLength = lastCluster - textRange.Start; + + return measuredLength == 0 ? 1 : measuredLength; + } + + lastCluster = glyphInfo.GlyphCluster; + currentWidth += glyphInfo.GlyphAdvance; + } + + break; } - lastCluster = glyphInfo.GlyphCluster; - currentWidth += glyphInfo.GlyphAdvance; + case { } drawableTextRun: + { + if (currentWidth + drawableTextRun.Size.Width > paragraphWidth) + { + var measuredLength = lastCluster - textRange.Start; + + return measuredLength == 0 ? 1 : measuredLength; + } + + lastCluster += currentRun.TextSourceLength; + currentWidth += currentRun.Size.Width; + + break; + } } } @@ -452,7 +537,7 @@ namespace Avalonia.Media.TextFormatting /// /// 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, TextRange textRange, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection, TextLineBreak? currentLineBreak) { @@ -568,7 +653,7 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitShapedRuns(textRuns, measuredLength); + var splitResult = SplitDrawableRuns(textRuns, measuredLength); textRange = new TextRange(textRange.Start, measuredLength); @@ -644,7 +729,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..fa4725ad64 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -193,187 +193,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.TextRange.Start + textLine.TextRange.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; - - 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 textBounds = textLine.GetTextBounds(start, length); - 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.TextRange.Start + textLine.TextRange.Length >= start + length) { break; } @@ -541,7 +388,7 @@ namespace Avalonia.Media.TextFormatting var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); - var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; + var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; var textRange = new TextRange(startingIndex, 1); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 74c4573630..838ff82ab2 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,105 @@ 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); + if (measuredLength > 0) + { + List? preSplitRuns = null; + List? postSplitRuns; - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; - } - else - { - postSplitRuns = shapedTextRuns; - } + if (_prefixLength > 0) + { + var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, + Math.Min(_prefixLength, measuredLength)); - shapedTextCharacters.Add(shapedSymbol); + collapsedRuns.AddRange(splitResult.First); - if (measuredLength > _prefixLength && postSplitRuns is not null) - { - var availableSuffixWidth = availableWidth; + TextLineImpl.SortRuns(collapsedRuns); - 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; + } - if (suffixCount > 0) + var availableSuffixWidth = availableWidth; + + if (preSplitRuns is not null) + { + foreach (var run in preSplitRuns) { - var splitSuffix = run.Split(run.TextSourceLength - suffixCount); + availableSuffixWidth -= run.Size.Width; + } + } - shapedTextCharacters.Add(splitSuffix.Second!); + for (var i = postSplitRuns.Count - 1; i >= 0; i--) + { + var run = postSplitRuns[i]; + + 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..d163c228b2 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -189,6 +189,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..fe52fbeef5 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -9,24 +9,32 @@ namespace Avalonia.Media.TextFormatting { 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 static readonly Comparison s_compareLogicalOrder = + (a, b) => + { + if (a is ShapedTextCharacters && b is ShapedTextCharacters) + { + return s_compareStart.Compare(a.Text.Start, b.Text.Start); + } - private readonly List _textRuns; + return 0; + }; + + 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, TextRange textRange, double paragraphWidth, + TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { TextRange = textRange; TextLineBreak = lineBreak; HasCollapsed = hasCollapsed; - _textRuns = textRuns; + _textRuns = textRuns; _paragraphWidth = paragraphWidth; _paragraphProperties = paragraphProperties; @@ -88,7 +96,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 +104,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 +140,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, TextRange, _paragraphWidth, _paragraphProperties, + _flowDirection, TextLineBreak, true); - return collapsedLine; + if (collapsedRuns.Count > 0) + { + collapsedLine.FinalizeLine(); } - return this; + return collapsedLine; + } /// public override CharacterHit GetCharacterHitFromDistance(double distance) { distance -= Start; - + if (distance <= 0) { + if (_textRuns.Count == 0) + { + return _flowDirection == FlowDirection.LeftToRight ? + new CharacterHit(TextRange.Start) : + new CharacterHit(TextRange.Start, TextRange.Length); + } + // 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(TextRange.Start) : + new CharacterHit(TextRange.Start + TextRange.Length); } // process hit that happens within the line var characterHit = new CharacterHit(); - 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(currentRun.Text.Start); + } + else + { + characterHit = new CharacterHit(currentRun.Text.Start, currentRun.Text.Length); + } + break; + } + } - if (distance <= run.Size.Width) + if (distance <= currentRun.Size.Width) { break; } - distance -= run.Size.Width; + distance -= currentRun.Size.Width; } return characterHit; @@ -166,83 +233,105 @@ namespace Avalonia.Media.TextFormatting for (var index = 0; index < _textRuns.Count; index++) { var textRun = _textRuns[index]; - var currentRun = textRun.GlyphRun; - - if (lastRun != null) - { - 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) + switch (textRun) { - 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.End) + { + 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 == textRun.Text.Start) + { + return currentDistance; + } + + break; } - } } //No hit hit found so we add the full width - currentDistance += currentRun.Size.Width; - - lastRun = currentRun; + currentDistance += textRun.Size.Width; } return currentDistance; @@ -251,6 +340,15 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { + if (_textRuns.Count == 0) + { + var textPosition = TextRange.Start + TextRange.Length; + + return characterHit.FirstCharacterIndex + characterHit.TrailingLength == textPosition ? + characterHit : + new CharacterHit(textPosition); + } + if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) { return nextCharacterHit; @@ -259,11 +357,23 @@ namespace Avalonia.Media.TextFormatting // Can't move, we're after the last character var runIndex = GetRunIndexAtCharacterIndex(TextRange.End, LogicalDirection.Forward); - 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(currentRun.Text.Start, currentRun.Text.Length); + break; + } + } - return characterHit; + return characterHit; } /// @@ -289,7 +399,202 @@ namespace Avalonia.Media.TextFormatting return GetPreviousCaretCharacterHit(characterHit); } - public static void SortRuns(List textRuns) + public override IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength) + { + if (firstTextSourceCharacterIndex + textLength <= TextRange.Start) + { + 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 static void SortRuns(List textRuns) { textRuns.Sort(s_compareLogicalOrder); } @@ -303,18 +608,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 +655,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 +692,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(); } } @@ -479,36 +805,55 @@ namespace Avalonia.Media.TextFormatting while (runIndex < _textRuns.Count) { - var run = _textRuns[runIndex]; + var currentRun = _textRuns[runIndex]; - var foundCharacterHit = - run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, - out _); + switch (currentRun) + { + case ShapedTextCharacters shapedRun: + { + var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); - var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == - TextRange.Start + TextRange.Length; + var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == + TextRange.Start + TextRange.Length; - if (isAtEnd && !run.GlyphRun.IsLeftToRight) - { - nextCharacterHit = foundCharacterHit; + if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight) + { + nextCharacterHit = foundCharacterHit; - return true; - } + return true; + } - var characterIndex = codepointIndex - run.Text.Start; + var characterIndex = codepointIndex - shapedRun.Text.Start; - if (characterIndex < 0 && run.ShapedBuffer.IsLeftToRight) - { - foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); - } + if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight) + { + foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); + } - nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? - foundCharacterHit : - new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); + nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? + foundCharacterHit : + new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); - if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex) - { - return true; + if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex) + { + return true; + } + + break; + } + default: + { + var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (textPosition == currentRun.Text.Start) + { + nextCharacterHit = new CharacterHit(currentRun.Text.Start + currentRun.Text.Length); + + return true; + } + + break; + } } runIndex++; @@ -545,25 +890,46 @@ namespace Avalonia.Media.TextFormatting 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 == currentRun.Text.Start + currentRun.Text.Length) + { + previousCharacterHit = new CharacterHit(currentRun.Text.Start); + + return true; + } + + break; + } } runIndex--; @@ -581,54 +947,81 @@ namespace Avalonia.Media.TextFormatting private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction) { var runIndex = 0; - ShapedTextCharacters? previousRun = null; + 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) + { + runIndex++; + previousRun = currentRun; + } + else + { + break; + } + break; } - } - else - { - if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length) + + default: { - return --runIndex; - } - } - } + if (codepointIndex == currentRun.Text.Start) + { + return runIndex; + } - if (direction == LogicalDirection.Forward) - { - if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End) - { - return runIndex; - } - } - else - { - if (codepointIndex > currentRun.Text.Start && - codepointIndex <= currentRun.Text.Start + currentRun.Text.Length) - { - return runIndex; - } - } + if (runIndex + 1 < _textRuns.Count) + { + runIndex++; + previousRun = currentRun; + } + else + { + return runIndex; + } - if (runIndex + 1 < _textRuns.Count) - { - runIndex++; - previousRun = currentRun; - } - else - { - break; + break; + } } } @@ -637,6 +1030,8 @@ namespace Avalonia.Media.TextFormatting private TextLineMetrics CreateLineMetrics() { + var start = 0d; + var height = 0d; var width = 0d; var widthIncludingWhitespace = 0d; var trailingWhitespaceLength = 0; @@ -646,76 +1041,137 @@ 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; + } + } - 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; - - width = widthIncludingWhitespace + - textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset; + 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; + } + } - trailingWhitespaceLength = firstRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = firstRun.GlyphRun.Metrics.NewlineLength; + if (ascent > -drawableTextRun.Size.Height) + { + ascent = -drawableTextRun.Size.Height; } 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) ? + height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ? descent - ascent + lineGap : lineHeight; @@ -725,15 +1181,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/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/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.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..35a47524f3 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -508,7 +508,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 +562,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.TextRange.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/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 367e6f4bea..fa04184edc 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -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,144 @@ 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 defaultTextRunProperties = - new GenericTextRunProperties(Typeface.Default); + 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)); - const string text = "0123456789"; + Assert.Equal(3, currentHit.FirstCharacterIndex); + Assert.Equal(0, currentHit.TrailingLength); - var source = new SingleBufferTextSource(text, defaultTextRunProperties); + currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties); + Assert.Equal(2, currentHit.FirstCharacterIndex); + Assert.Equal(0, currentHit.TrailingLength); - var formatter = TextFormatter.Current; + currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties); + Assert.Equal(1, currentHit.FirstCharacterIndex); + Assert.Equal(0, currentHit.TrailingLength); - Assert.NotNull(textLine.TextLineBreak.TextEndOfLine); + 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(3, characterHit.FirstCharacterIndex); + Assert.Equal(0, characterHit.TrailingLength); + } + } + + private class DrawableRunTextSource : ITextSource + { + const string Text = "A_A_"; + + public TextRun? GetTextRun(int textSourceIndex) + { + switch (textSourceIndex) + { + case 0: + return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 0, 1), new GenericTextRunProperties(Typeface.Default)); + case 1: + return new CustomDrawableRun(1); + case 2: + return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 2, 1, 2), new GenericTextRunProperties(Typeface.Default)); + case 3: + return new CustomDrawableRun(3); + default: + return null; + } + } + } + + private class CustomDrawableRun : DrawableTextRun + { + public CustomDrawableRun(int start) + { + Text = new ReadOnlySlice(new char[1], start, DefaultTextSourceLength); + } + + public override ReadOnlySlice Text { get; } + + public override Size Size => new(14, 14); + public override double Baseline => 14; + public override void Draw(DrawingContext drawingContext, Point origin) + { + } } @@ -517,6 +639,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;) From b99e1b6b2c0c410b9b093bc26fe382d88f9c84b3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 29 Mar 2022 11:48:00 +0200 Subject: [PATCH 02/17] Cleanup attached properties --- .../DataGridTextColumn.cs | 23 ++++- src/Avalonia.Controls/Control.cs | 90 ------------------ .../Presenters/TextPresenter.cs | 55 +++++++++++ .../Primitives/TemplatedControl.cs | 91 +++++++++++++++++++ src/Avalonia.Controls/TextBlock.cs | 90 ++++++++++++++++++ .../Controls/Button.xaml | 2 +- .../Controls/CaptionButtons.xaml | 2 +- .../Controls/CheckBox.xaml | 2 +- .../Controls/DatePicker.xaml | 4 +- .../Controls/RepeatButton.xaml | 2 +- .../Controls/CaptionButtons.xaml | 2 +- .../Controls/ComboBox.xaml | 2 +- .../Controls/ComboBoxItem.xaml | 2 +- .../Controls/DatePicker.xaml | 4 +- .../Controls/RadioButton.xaml | 2 +- .../Controls/Slider.xaml | 4 +- .../Controls/TimePicker.xaml | 6 +- 17 files changed, 272 insertions(+), 111 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs index e7c8e67ff0..863910c226 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls /// Identifies the FontFamily dependency property. /// public static readonly AttachedProperty FontFamilyProperty = - TextBlock.FontFamilyProperty.AddOwner(); + TextElement.FontFamilyProperty.AddOwner(); /// /// Gets or sets the font name. @@ -48,7 +48,7 @@ namespace Avalonia.Controls /// Identifies the FontSize dependency property. /// public static readonly AttachedProperty FontSizeProperty = - TextBlock.FontSizeProperty.AddOwner(); + TextElement.FontSizeProperty.AddOwner(); /// /// Gets or sets the font size. @@ -65,7 +65,7 @@ namespace Avalonia.Controls /// Identifies the FontStyle dependency property. /// public static readonly AttachedProperty FontStyleProperty = - TextBlock.FontStyleProperty.AddOwner(); + TextElement.FontStyleProperty.AddOwner(); /// /// Gets or sets the font style. @@ -80,7 +80,7 @@ namespace Avalonia.Controls /// Identifies the FontWeight dependency property. /// public static readonly AttachedProperty FontWeightProperty = - TextBlock.FontWeightProperty.AddOwner(); + TextElement.FontWeightProperty.AddOwner(); /// /// Gets or sets the font weight or thickness. @@ -91,6 +91,21 @@ namespace Avalonia.Controls set => SetValue(FontWeightProperty, value); } + /// + /// Identifies the FontStretch dependency property. + /// + public static readonly AttachedProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Gets or sets the font weight or thickness. + /// + public FontStretch FontStretch + { + get => GetValue(FontStretchProperty); + set => SetValue(FontStretchProperty, value); + } + /// /// Identifies the Foreground dependency property. /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 3eb1f1c472..45c0d2948d 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -68,42 +68,6 @@ namespace Avalonia.Controls public static readonly AttachedProperty FlowDirectionProperty = AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); - /// - /// Defines the property. - /// - public static readonly AttachedProperty FontFamilyProperty = - TextElement.FontFamilyProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly AttachedProperty FontSizeProperty = - TextElement.FontSizeProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly AttachedProperty FontStyleProperty = - TextElement.FontStyleProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly AttachedProperty FontWeightProperty = - TextElement.FontWeightProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly AttachedProperty FontStretchProperty = - TextElement.FontStretchProperty.AddOwner(); - - /// - /// Defines the property. - /// - public static readonly AttachedProperty ForegroundProperty = - TextElement.ForegroundProperty.AddOwner(); - private DataTemplates? _dataTemplates; private IControl? _focusAdorner; private AutomationPeer? _automationPeer; @@ -162,60 +126,6 @@ namespace Avalonia.Controls set => SetValue(FlowDirectionProperty, value); } - /// - /// Gets or sets the font family. - /// - public FontFamily FontFamily - { - get { return GetValue(FontFamilyProperty); } - set { SetValue(FontFamilyProperty, value); } - } - - /// - /// Gets or sets the font size. - /// - public double FontSize - { - get { return GetValue(FontSizeProperty); } - set { SetValue(FontSizeProperty, value); } - } - - /// - /// Gets or sets the font style. - /// - public FontStyle FontStyle - { - get { return GetValue(FontStyleProperty); } - set { SetValue(FontStyleProperty, value); } - } - - /// - /// Gets or sets the font weight. - /// - public FontWeight FontWeight - { - get { return GetValue(FontWeightProperty); } - set { SetValue(FontWeightProperty, value); } - } - - /// - /// Gets or sets the font stretch. - /// - public FontStretch FontStretch - { - get { return GetValue(FontStretchProperty); } - set { SetValue(FontStretchProperty, value); } - } - - /// - /// Gets or sets a brush used to paint the text. - /// - public IBrush? Foreground - { - get { return GetValue(ForegroundProperty); } - set { SetValue(ForegroundProperty, value); } - } - /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 5267cc1d6e..ebd0196b80 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -8,6 +8,7 @@ using Avalonia.Utilities; using Avalonia.VisualTree; using Avalonia.Layout; using Avalonia.Media.Immutable; +using Avalonia.Controls.Documents; namespace Avalonia.Controls.Presenters { @@ -116,6 +117,60 @@ namespace Avalonia.Controls.Presenters set => SetAndRaise(TextProperty, ref _text, value); } + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get => TextElement.GetFontFamily(this); + set => TextElement.SetFontFamily(this, value); + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => TextElement.GetFontSize(this); + set => TextElement.SetFontSize(this, value); + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get => TextElement.GetFontStyle(this); + set => TextElement.SetFontStyle(this, value); + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get => TextElement.GetFontWeight(this); + set => TextElement.SetFontWeight(this, value); + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => TextElement.GetFontStretch(this); + set => TextElement.SetFontStretch(this, value); + } + + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get => TextElement.GetForeground(this); + set => TextElement.SetForeground(this, value); + } + /// /// Gets or sets the control's text wrapping mode. /// diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 212aaa27d6..795c307c54 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Documents; using Avalonia.Controls.Templates; using Avalonia.Interactivity; using Avalonia.Logging; @@ -37,6 +38,42 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty CornerRadiusProperty = Border.CornerRadiusProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + /// /// Defines the property. /// @@ -119,6 +156,60 @@ namespace Avalonia.Controls.Primitives set { SetValue(CornerRadiusProperty, value); } } + /// + /// Gets or sets the font family used to draw the control's text. + /// + public FontFamily FontFamily + { + get { return GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + /// + /// Gets or sets the size of the control's text in points. + /// + public double FontSize + { + get { return GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + /// + /// Gets or sets the font style used to draw the control's text. + /// + public FontStyle FontStyle + { + get { return GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + /// + /// Gets or sets the font weight used to draw the control's text. + /// + public FontWeight FontWeight + { + get { return GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + /// + /// Gets or sets the font stretch used to draw the control's text. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + /// + /// Gets or sets the brush used to draw the control's text and other foreground elements. + /// + public IBrush? Foreground + { + get { return GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + /// /// Gets or sets the padding placed between the border of the control and its content. /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 8b8f8bac37..2abaf14419 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -28,6 +28,42 @@ namespace Avalonia.Controls public static readonly StyledProperty PaddingProperty = Decorator.PaddingProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + /// /// DependencyProperty for property. /// @@ -174,6 +210,60 @@ namespace Avalonia.Controls [Content] public InlineCollection Inlines { get; } + /// + /// Gets or sets the font family used to draw the control's text. + /// + public FontFamily FontFamily + { + get { return GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + /// + /// Gets or sets the size of the control's text in points. + /// + public double FontSize + { + get { return GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + /// + /// Gets or sets the font style used to draw the control's text. + /// + public FontStyle FontStyle + { + get { return GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + /// + /// Gets or sets the font weight used to draw the control's text. + /// + public FontWeight FontWeight + { + get { return GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + /// + /// Gets or sets the font stretch used to draw the control's text. + /// + public FontStretch FontStretch + { + get { return GetValue(FontStretchProperty); } + set { SetValue(FontStretchProperty, value); } + } + + /// + /// Gets or sets the brush used to draw the control's text and other foreground elements. + /// + public IBrush? Foreground + { + get { return GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + /// /// Gets or sets the height of each line of content. /// diff --git a/src/Avalonia.Themes.Default/Controls/Button.xaml b/src/Avalonia.Themes.Default/Controls/Button.xaml index a8509f2976..3f8a94b005 100644 --- a/src/Avalonia.Themes.Default/Controls/Button.xaml +++ b/src/Avalonia.Themes.Default/Controls/Button.xaml @@ -18,7 +18,7 @@ Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" RecognizesAccessKey="True" - Foreground="{TemplateBinding Foreground}" + TextElement.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml index af56698ad5..9c1d4d0b1d 100644 --- a/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml @@ -4,7 +4,7 @@ - + diff --git a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml index 9520857b02..47398966f7 100644 --- a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml @@ -6,7 +6,7 @@ Value="{DynamicResource ThemeBorderLowBrush}" /> - diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml index 9af1c073cd..2d65ea2b7b 100644 --- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml @@ -56,7 +56,7 @@ @@ -160,7 +160,7 @@ @@ -172,7 +172,7 @@ @@ -184,7 +184,7 @@ @@ -196,7 +196,7 @@ @@ -208,7 +208,7 @@ @@ -219,7 +219,7 @@ @@ -230,7 +230,7 @@ @@ -242,7 +242,7 @@ @@ -254,7 +254,7 @@ @@ -266,6 +266,6 @@ diff --git a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml index 5a6eb15f2d..0a5147e335 100644 --- a/src/Avalonia.Themes.Default/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Default/Controls/TimePicker.xaml @@ -33,7 +33,7 @@ @@ -185,11 +185,11 @@ @@ -203,11 +203,11 @@ @@ -216,11 +216,11 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml index 19e5e29c58..9b8fda0874 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml @@ -18,7 +18,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index 8840397843..0e8bde43c8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -32,7 +32,7 @@ @@ -136,7 +136,7 @@ @@ -148,7 +148,7 @@ @@ -160,7 +160,7 @@ @@ -172,7 +172,7 @@ @@ -184,7 +184,7 @@ @@ -195,7 +195,7 @@ @@ -206,7 +206,7 @@ @@ -218,7 +218,7 @@ @@ -230,7 +230,7 @@ @@ -242,6 +242,6 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml index 20039fd71b..df8f691490 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml @@ -19,7 +19,7 @@ - + @@ -61,7 +61,7 @@