Browse Source

Merge pull request #11447 from Gillibald/fixTextBoxMaxLines

Rework TextBox.MaxLines
pull/11523/head
Max Katz 3 years ago
committed by GitHub
parent
commit
1c041720d7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  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; _textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
MaxWidth = maxWidth; MaxWidth = maxWidth;
MaxHeight = maxHeight; MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines; MaxLines = maxLines;
_textLines = CreateTextLines(); _textLines = CreateTextLines();
@ -81,8 +77,6 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textTrimming">The text trimming.</param> /// <param name="textTrimming">The text trimming.</param>
/// <param name="maxWidth">The maximum width.</param> /// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</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> /// <param name="maxLines">The maximum number of text lines.</param>
public TextLayout( public TextLayout(
ITextSource textSource, ITextSource textSource,
@ -90,8 +84,6 @@ namespace Avalonia.Media.TextFormatting
TextTrimming? textTrimming = null, TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity, double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0) int maxLines = 0)
{ {
_textSource = textSource; _textSource = textSource;
@ -100,14 +92,10 @@ namespace Avalonia.Media.TextFormatting
_textTrimming = textTrimming ?? TextTrimming.None; _textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
MaxWidth = maxWidth; MaxWidth = maxWidth;
MaxHeight = maxHeight; MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines; MaxLines = maxLines;
_textLines = CreateTextLines(); _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 /// 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. /// is determined automatically from the current font characteristics. The default is NaN.
/// </remarks> /// </remarks>
public double LineHeight { get; } public double LineHeight => _paragraphProperties.LineHeight;
/// <summary> /// <summary>
/// Gets the maximum width. /// Gets the maximum width.
@ -140,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Gets the text spacing. /// Gets the text spacing.
/// </summary> /// </summary>
public double LetterSpacing { get; } public double LetterSpacing => _paragraphProperties.LetterSpacing;
/// <summary> /// <summary>
/// Gets the text lines. /// 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="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param> /// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <returns></returns> /// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
double letterSpacing) double letterSpacing)

3
src/Avalonia.Controls/TextBlock.cs

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

91
src/Avalonia.Controls/TextBox.cs

@ -18,9 +18,6 @@ using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Platform;
using System.Reflection;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -28,6 +25,7 @@ namespace Avalonia.Controls
/// Represents a control that can be used to display or edit unformatted text. /// Represents a control that can be used to display or edit unformatted text.
/// </summary> /// </summary>
[TemplatePart("PART_TextPresenter", typeof(TextPresenter))] [TemplatePart("PART_TextPresenter", typeof(TextPresenter))]
[TemplatePart("PART_ScrollViewer", typeof(ScrollViewer))]
[PseudoClasses(":empty")] [PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{ {
@ -158,7 +156,7 @@ namespace Avalonia.Controls
/// Defines see <see cref="TextPresenter.LineHeight"/> property. /// Defines see <see cref="TextPresenter.LineHeight"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<double> LineHeightProperty = public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextBox>(); TextBlock.LineHeightProperty.AddOwner<TextBox>(new(defaultValue: double.NaN));
/// <summary> /// <summary>
/// Defines see <see cref="TextBlock.LetterSpacing"/> property. /// Defines see <see cref="TextBlock.LetterSpacing"/> property.
@ -310,6 +308,7 @@ namespace Avalonia.Controls
} }
private TextPresenter? _presenter; private TextPresenter? _presenter;
private ScrollViewer? _scrollViewer;
private readonly TextBoxTextInputMethodClient _imClient = new(); private readonly TextBoxTextInputMethodClient _imClient = new();
private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper; private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing; private bool _isUndoingRedoing;
@ -490,7 +489,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets or sets the maximum character length of the TextBox /// Gets or sets the maximum number of visible lines.
/// </summary> /// </summary>
public int MaxLength public int MaxLength
{ {
@ -803,6 +802,8 @@ namespace Avalonia.Controls
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
_imClient.SetPresenter(_presenter, this); _imClient.SetPresenter(_presenter, this);
if (IsFocused) if (IsFocused)
@ -855,6 +856,10 @@ namespace Avalonia.Controls
{ {
OnSelectionEndChanged(change); OnSelectionEndChanged(change);
} }
else if (change.Property == MaxLinesProperty)
{
InvalidateMeasure();
}
else if (change.Property == UndoLimitProperty) else if (change.Property == UndoLimitProperty)
{ {
OnUndoLimitChanged(change.GetNewValue<int>()); OnUndoLimitChanged(change.GetNewValue<int>());
@ -942,40 +947,10 @@ namespace Avalonia.Controls
{ {
return; return;
} }
_selectedTextChangesMadeSinceLastUndoSnapshot++; _selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false); 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 currentText = Text ?? string.Empty;
var selectionLength = Math.Abs(SelectionStart - SelectionEnd); var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
var newLength = input.Length + currentText.Length - selectionLength; var newLength = input.Length + currentText.Length - selectionLength;
@ -1518,7 +1493,7 @@ namespace Avalonia.Controls
_presenter.MoveCaretToPoint(point); _presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex; var caretIndex = _presenter.CaretIndex;
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -1976,5 +1951,47 @@ namespace Avalonia.Controls
{ {
CanRedo = _undoRedoHelper.CanRedo; 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" IsVisible="False"
Text="{TemplateBinding Watermark}" Text="{TemplateBinding Watermark}"
DockPanel.Dock="Top" /> DockPanel.Dock="Top" />
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" <ScrollViewer Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"

3
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@ -123,7 +123,8 @@
<ContentPresenter Grid.Column="0" <ContentPresenter Grid.Column="0"
Grid.ColumnSpan="1" Grid.ColumnSpan="1"
Content="{TemplateBinding InnerLeftContent}" /> Content="{TemplateBinding InnerLeftContent}" />
<ScrollViewer Grid.Column="1" <ScrollViewer Name="PART_ScrollViewer"
Grid.Column="1"
Grid.ColumnSpan="1" Grid.ColumnSpan="1"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}" 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."); 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 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 return new Border
{ {

19
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

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

Loading…
Cancel
Save