diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 431be00f4e..8009492d77 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -124,6 +124,12 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxLinesProperty = AvaloniaProperty.Register(nameof(MaxLines)); + /// + /// Defines the property + /// + public static readonly StyledProperty MinLinesProperty = + AvaloniaProperty.Register(nameof(MinLines)); + /// /// Defines the property /// @@ -519,6 +525,15 @@ namespace Avalonia.Controls set => SetValue(MaxLinesProperty, value); } + /// + /// Gets or sets the minimum number of visible lines to size to. + /// + public int MinLines + { + get => GetValue(MinLinesProperty); + set => SetValue(MinLinesProperty, value); + } + /// /// Gets or sets the spacing between characters /// @@ -913,6 +928,10 @@ namespace Avalonia.Controls { InvalidateMeasure(); } + else if (change.Property == MinLinesProperty) + { + InvalidateMeasure(); + } else if (change.Property == UndoLimitProperty) { OnUndoLimitChanged(change.GetNewValue()); @@ -1836,7 +1855,7 @@ namespace Avalonia.Controls } SetCurrentValue(SelectionEndProperty, SelectionEnd + offset); - + if (moveCaretPosition) { _presenter.MoveCaretToTextPosition(SelectionEnd); @@ -2034,7 +2053,7 @@ namespace Avalonia.Controls var margin = visual.GetValue(Layoutable.MarginProperty); var padding = visual.GetValue(Decorator.PaddingProperty); - + verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom; visual = visual.VisualParent; @@ -2073,8 +2092,8 @@ namespace Avalonia.Controls var selectionStart = CaretIndex; MoveHorizontal(-1, true, false, false); - - if (SelectionEnd > 0 && + + if (SelectionEnd > 0 && selectionStart < text.Length && text[selectionStart] == ' ') { SetCurrentValue(SelectionEndProperty, SelectionEnd - 1); @@ -2203,30 +2222,46 @@ namespace Avalonia.Controls 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); + var textLayout = new TextLayout(new LineTextSource(MaxLines), paragraphProperties); var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); maxHeight = Math.Ceiling(textLayout.Height + verticalSpace); } _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight); + + + var minHeight = 0.0; + + if (MinLines > 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 LineTextSource(MinLines), paragraphProperties); + var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); + + minHeight = Math.Ceiling(textLayout.Height + verticalSpace); + } + + _scrollViewer.SetCurrentValue(MinHeightProperty, minHeight); } return base.MeasureOverride(availableSize); } - private class MaxLinesTextSource : ITextSource + private class LineTextSource : ITextSource { - private readonly int _maxLines; + private readonly int _lines; - public MaxLinesTextSource(int maxLines) + public LineTextSource(int lines) { - _maxLines = maxLines; + _lines = lines; } public TextRun? GetTextRun(int textSourceIndex) { - if (textSourceIndex >= _maxLines) + if (textSourceIndex >= _lines) { return null; } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index a53d1dd5a1..00d13c6cc7 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1066,6 +1066,118 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Should_Fullfill_MinLines_Contraint() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "ABC \n DEF \n GHI", + MinLines = 3, + AcceptsReturn = true + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate() + }; + topLevel.Content = target; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + target.ApplyTemplate(); + target.Measure(Size.Infinity); + + var initialHeight = target.DesiredSize.Height; + + target.Text = ""; + + target.InvalidateMeasure(); + target.Measure(Size.Infinity); + + Assert.Equal(initialHeight, target.DesiredSize.Height); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MinLines_Sets_ScrollViewer_MinHeight(int minLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MinLines = minLines, + + // Define explicit whole number line height for predictable calculations + LineHeight = 20 + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate(), + Content = target + }; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + var textPresenter = target.FindDescendantOfType(); + Assert.Equal("PART_TextPresenter", textPresenter.Name); + Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter + + var scrollViewer = target.FindDescendantOfType(); + Assert.Equal("PART_ScrollViewer", scrollViewer.Name); + Assert.Equal(minLines * target.LineHeight, scrollViewer.MinHeight); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MinLines_Sets_ScrollViewer_MinHeight_With_TextPresenter_Margin(int minLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MinLines = minLines, + + // Define explicit whole number line height for predictable calculations + LineHeight = 20 + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate(), + Content = target + }; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + var textPresenter = target.FindDescendantOfType(); + Assert.Equal("PART_TextPresenter", textPresenter.Name); + var textPresenterMargin = new Thickness(horizontal: 0, vertical: 3); + textPresenter.Margin = textPresenterMargin; + + target.InvalidateMeasure(); + target.Measure(Size.Infinity); + + var scrollViewer = target.FindDescendantOfType(); + Assert.Equal("PART_ScrollViewer", scrollViewer.Name); + Assert.Equal((minLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MinHeight); + } + } + [Fact] public void CanUndo_CanRedo_Is_False_When_Initialized() {