Quellcode durchsuchen

Rework MaxLines to limit how many lines are visible at once and don't prevent additional input if MaxLines is reached

Benedikt Stebner vor 2 Jahren
Ursprung
Commit
e69661b353

+ 3 - 15
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -60,14 +60,10 @@ namespace Avalonia.Media.TextFormatting
 
             _textTrimming = textTrimming ?? TextTrimming.None;
 
-            LineHeight = lineHeight;
-
             MaxWidth = maxWidth;
 
             MaxHeight = maxHeight;
 
-            LetterSpacing = letterSpacing;
-
             MaxLines = maxLines;
 
             _textLines = CreateTextLines();
@@ -81,8 +77,6 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textTrimming">The text trimming.</param>
         /// <param name="maxWidth">The maximum width.</param>
         /// <param name="maxHeight">The maximum height.</param>
-        /// <param name="lineHeight">The height of each line of text.</param>
-        /// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
         /// <param name="maxLines">The maximum number of text lines.</param>
         public TextLayout(
             ITextSource textSource,
@@ -90,8 +84,6 @@ namespace Avalonia.Media.TextFormatting
             TextTrimming? textTrimming = null,
             double maxWidth = double.PositiveInfinity,
             double maxHeight = double.PositiveInfinity,
-            double lineHeight = double.NaN,
-            double letterSpacing = 0,
             int maxLines = 0)
         {
             _textSource = textSource;
@@ -100,14 +92,10 @@ namespace Avalonia.Media.TextFormatting
 
             _textTrimming = textTrimming ?? TextTrimming.None;
 
-            LineHeight = lineHeight;
-
             MaxWidth = maxWidth;
 
             MaxHeight = maxHeight;
 
-            LetterSpacing = letterSpacing;
-
             MaxLines = maxLines;
 
             _textLines = CreateTextLines();
@@ -120,7 +108,7 @@ namespace Avalonia.Media.TextFormatting
         /// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height
         /// is determined automatically from the current font characteristics. The default is NaN.
         /// </remarks>
-        public double LineHeight { get; }
+        public double LineHeight => _paragraphProperties.LineHeight;
 
         /// <summary>
         /// Gets the maximum width.
@@ -140,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Gets the text spacing.
         /// </summary>
-        public double LetterSpacing { get; }
+        public double LetterSpacing  => _paragraphProperties.LetterSpacing;
 
         /// <summary>
         /// Gets the text lines.
@@ -495,7 +483,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="lineHeight">The height of each line of text.</param>
         /// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
         /// <returns></returns>
-        private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
+        internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
             IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
             TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
             double letterSpacing)

+ 1 - 2
src/Avalonia.Controls/TextBlock.cs

@@ -639,8 +639,7 @@ namespace Avalonia.Controls
                 TextTrimming,
                 _constraint.Width,
                 _constraint.Height,
-                maxLines: MaxLines,
-                lineHeight: LineHeight);
+                MaxLines);
         }
 
         /// <summary>

+ 54 - 37
src/Avalonia.Controls/TextBox.cs

@@ -18,9 +18,6 @@ using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Automation.Peers;
 using Avalonia.Threading;
-using Avalonia.Platform;
-using System.Reflection;
-using static System.Net.Mime.MediaTypeNames;
 
 namespace Avalonia.Controls
 {
@@ -28,6 +25,7 @@ namespace Avalonia.Controls
     /// Represents a control that can be used to display or edit unformatted text.
     /// </summary>
     [TemplatePart("PART_TextPresenter", typeof(TextPresenter))]
+    [TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))]
     [PseudoClasses(":empty")]
     public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
     {
@@ -158,7 +156,7 @@ namespace Avalonia.Controls
         /// Defines see <see cref="TextPresenter.LineHeight"/> property.
         /// </summary>
         public static readonly StyledProperty<double> LineHeightProperty =
-            TextBlock.LineHeightProperty.AddOwner<TextBox>();
+            TextBlock.LineHeightProperty.AddOwner<TextBox>(new(defaultValue: double.NaN));
 
         /// <summary>
         /// Defines see <see cref="TextBlock.LetterSpacing"/> property.
@@ -310,6 +308,7 @@ namespace Avalonia.Controls
         }
 
         private TextPresenter? _presenter;
+        private ScrollViewer? _scrollViewer;
         private readonly TextBoxTextInputMethodClient _imClient = new();
         private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper;
         private bool _isUndoingRedoing;
@@ -490,7 +489,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Gets or sets the maximum character length of the TextBox
+        /// Gets or sets the maximum number of visible lines.
         /// </summary>
         public int MaxLength
         {
@@ -803,6 +802,8 @@ namespace Avalonia.Controls
         {
             _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
 
+            _scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
+
             _imClient.SetPresenter(_presenter, this);
 
             if (IsFocused)
@@ -855,6 +856,10 @@ namespace Avalonia.Controls
             {
                 OnSelectionEndChanged(change);
             }
+            else if (change.Property == MaxLinesProperty)
+            {
+                InvalidateMeasure();
+            }
             else if (change.Property == UndoLimitProperty)
             {
                 OnUndoLimitChanged(change.GetNewValue<int>());
@@ -942,40 +947,10 @@ namespace Avalonia.Controls
             {
                 return;
             }
+
             _selectedTextChangesMadeSinceLastUndoSnapshot++;
             SnapshotUndoRedo(ignoreChangeCount: false);
 
-            if (_presenter != null && MaxLines > 0)
-            {
-                var lineCount = _presenter.TextLayout.TextLines.Count;
-
-                var length = 0;
-
-                var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan());
-
-                while (graphemeEnumerator.MoveNext(out var grapheme))
-                {
-                    if (grapheme.FirstCodepoint.IsBreakChar)
-                    {
-                        if (lineCount + 1 > MaxLines)
-                        {
-                            break;
-                        }
-                        else
-                        {
-                            lineCount++;
-                        }
-                    }
-
-                    length += grapheme.Length;
-                }
-
-                if (length < input.Length)
-                {
-                    input = input.Remove(Math.Max(0, length));
-                }
-            }
-
             var currentText = Text ?? string.Empty;
             var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
             var newLength = input.Length + currentText.Length - selectionLength;
@@ -1518,7 +1493,7 @@ namespace Avalonia.Controls
                 _presenter.MoveCaretToPoint(point);
 
                 var caretIndex = _presenter.CaretIndex;
-         
+
                 var selectionStart = SelectionStart;
                 var selectionEnd = SelectionEnd;
 
@@ -1976,5 +1951,47 @@ namespace Avalonia.Controls
         {
             CanRedo = _undoRedoHelper.CanRedo;
         }
+
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if(_scrollViewer != null)
+            {
+                var maxHeight = double.PositiveInfinity;
+
+                if (MaxLines > 0 && double.IsNaN(Height))
+                {
+                    var fontSize = FontSize;
+                    var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
+                    var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
+                    var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties);
+
+                    maxHeight = Math.Ceiling(textLayout.Height);
+                }
+
+                _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);
+            }
+
+            return base.MeasureOverride(availableSize);
+        }
+
+        private class MaxLinesTextSource : ITextSource
+        {
+            private readonly int _maxLines;
+
+            public MaxLinesTextSource(int maxLines)
+            {
+                _maxLines = maxLines;
+            }
+
+            public TextRun? GetTextRun(int textSourceIndex)
+            {
+                if (textSourceIndex >= _maxLines)
+                {
+                    return null;
+                }
+
+                return new TextEndOfLine(1);
+            }
+        }
     }
 }

+ 2 - 1
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@@ -133,7 +133,8 @@
                              IsVisible="False"
                              Text="{TemplateBinding Watermark}"
                              DockPanel.Dock="Top" />
-                  <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
+                  <ScrollViewer Name="PART_ScrollViewer"
+                                HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                                 VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
                                 IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
                                 AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"

+ 2 - 1
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@@ -123,7 +123,8 @@
                 <ContentPresenter Grid.Column="0"
                                   Grid.ColumnSpan="1"
                                   Content="{TemplateBinding InnerLeftContent}" />
-                <ScrollViewer Grid.Column="1"
+                <ScrollViewer Name="PART_ScrollViewer"
+                              Grid.Column="1"
                               Grid.ColumnSpan="1"
                               AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
                               BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"

+ 2 - 2
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@@ -440,7 +440,7 @@ namespace Avalonia.Controls.UnitTests
             throw new InvalidOperationException("Could not get the point in root coordinates.");
         }
 
-        private Control CreateTemplate(ScrollViewer control, INameScope scope)
+        internal static Control CreateTemplate(ScrollViewer control, INameScope scope)
         {
             return new Grid
             {
@@ -480,7 +480,7 @@ namespace Avalonia.Controls.UnitTests
             };
         }
 
-        private Control CreateScrollBarTemplate(ScrollBar scrollBar, INameScope scope)
+        private static Control CreateScrollBarTemplate(ScrollBar scrollBar, INameScope scope)
         {
             return new Border
             {

+ 15 - 4
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -884,7 +884,7 @@ namespace Avalonia.Controls.UnitTests
                     Template = CreateTemplate(),
                     Text = "ABC",
                     MaxLines = 1,
-                    AcceptsReturn= true
+                    AcceptsReturn = true
                 };
 
                 var impl = CreateMockTopLevelImpl();
@@ -896,8 +896,11 @@ namespace Avalonia.Controls.UnitTests
                 topLevel.ApplyTemplate();
                 topLevel.LayoutManager.ExecuteInitialLayoutPass();
 
+                target.ApplyTemplate();
                 target.Measure(Size.Infinity);
 
+                var initialHeight = target.DesiredSize.Height;
+
                 topLevel.Clipboard?.SetTextAsync(Environment.NewLine).GetAwaiter().GetResult();
 
                 RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
@@ -905,7 +908,10 @@ namespace Avalonia.Controls.UnitTests
 
                 RaiseTextEvent(target, Environment.NewLine);
 
-                Assert.Equal("ABC", target.Text);
+                target.InvalidateMeasure();
+                target.Measure(Size.Infinity);
+
+                Assert.Equal(initialHeight, target.DesiredSize.Height);
             }
         }
 
@@ -1116,7 +1122,11 @@ namespace Avalonia.Controls.UnitTests
         private IControlTemplate CreateTemplate()
         {
             return new FuncControlTemplate<TextBox>((control, scope) =>
-                new TextPresenter
+            new ScrollViewer
+            {
+                Name = "Part_ScrollViewer",
+                Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate),
+                Content = new TextPresenter
                 {
                     Name = "PART_TextPresenter",
                     [!!TextPresenter.TextProperty] = new Binding
@@ -1133,7 +1143,8 @@ namespace Avalonia.Controls.UnitTests
                         Priority = BindingPriority.Template,
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                     }
-                }.RegisterInNameScope(scope));
+                }.RegisterInNameScope(scope)
+            }.RegisterInNameScope(scope));
         }
 
         private static void RaiseKeyEvent(TextBox textBox, Key key, KeyModifiers inputModifiers)