From 41bef0e991ebd3ffdc7a94725c1d6f66171fb4a5 Mon Sep 17 00:00:00 2001 From: Bill Henning Date: Fri, 18 Aug 2023 11:15:00 -0400 Subject: [PATCH 1/2] Fix for TextBox not accounting for space between ScrollViewer and TextPresenter when calculating MaxLines-based height --- src/Avalonia.Controls/TextBox.cs | 34 +++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 98b3b13c17f..8a5ac4e4953 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1879,6 +1879,37 @@ private string GetSelection() return text.Substring(start, end - start); } + /// + /// Returns the sum of any vertical whitespace added between the and in the control template. + /// + /// The total vertical whitespace. + private double GetVerticalSpaceBetweenScrollViewerAndPresenter() + { + var verticalSpace = 0.0; + if (_presenter != null) + { + Visual? visual = _presenter; + while ((visual != null) && (visual != this)) + { + if (visual == _scrollViewer) + { + // ScrollViewer is a stopping point and should only include the Padding + verticalSpace += _scrollViewer.Padding.Top + _scrollViewer.Padding.Bottom; + break; + } + + var margin = visual.GetValue(Layoutable.MarginProperty); + var padding = visual.GetValue(Decorator.PaddingProperty); + + verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom; + + visual = visual.VisualParent; + } + } + + return verticalSpace; + } + /// /// Raises both the and events. /// @@ -2032,8 +2063,9 @@ protected override Size MeasureOverride(Size availableSize) 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 verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); - maxHeight = Math.Ceiling(textLayout.Height); + maxHeight = Math.Ceiling(textLayout.Height + verticalSpace); } _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight); From 932b40b19b97c4a75ff7b244464b2a6774e93ac4 Mon Sep 17 00:00:00 2001 From: Boyd Patterson Date: Fri, 18 Aug 2023 11:53:42 -0500 Subject: [PATCH 2/2] Added unit tests for TextBox to verify MaxLines property assigns the ScrollViewer.MaxHeight based on line height. --- .../TextBoxTests.cs | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index d4558c9e04a..3c7fb6f9c03 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -15,6 +15,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -916,6 +917,82 @@ public void Should_Fullfill_MaxLines_Contraint() } } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MaxLines_Sets_ScrollViewer_MaxHeight(int maxLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MaxLines = maxLines, + + // 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(maxLines * target.LineHeight, scrollViewer.MaxHeight); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MaxLines_Sets_ScrollViewer_MaxHeight_With_TextPresenter_Margin(int maxLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MaxLines = maxLines, + + // 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((maxLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MaxHeight); + } + } + [Fact] public void CanUndo_CanRedo_Is_False_When_Initialized() { @@ -1125,7 +1202,7 @@ private IControlTemplate CreateTemplate() return new FuncControlTemplate((control, scope) => new ScrollViewer { - Name = "Part_ScrollViewer", + Name = "PART_ScrollViewer", Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate), Content = new TextPresenter {