Browse Source

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

pull/11447/head
Benedikt Stebner 3 years ago
parent
commit
e69661b353
  1. 18
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  2. 3
      src/Avalonia.Controls/TextBlock.cs
  3. 91
      src/Avalonia.Controls/TextBox.cs
  4. 3
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  5. 3
      src/Avalonia.Themes.Simple/Controls/TextBox.xaml
  6. 4
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  7. 19
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

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

3
src/Avalonia.Controls/TextBlock.cs

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

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

3
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)}"

3
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)}"

4
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
{

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

Loading…
Cancel
Save