Browse Source

Fix TextBlock/TextPresenter measure/arrange

Fix TextTrimming for small width
Fix text splitting
Fix text editing shortcuts
pull/7604/head
Benedikt Stebner 5 years ago
parent
commit
1db3296611
  1. 33
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  2. 2
      src/Avalonia.Controls/Primitives/AccessText.cs
  3. 63
      src/Avalonia.Controls/TextBlock.cs
  4. 120
      src/Avalonia.Controls/TextBox.cs
  5. 16
      src/Avalonia.Controls/Utils/StringUtils.cs
  6. 3
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  7. 9
      src/Avalonia.Visuals/Media/GlyphRun.cs
  8. 7
      src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs
  9. 10
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  10. 11
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  11. 62
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  12. 25
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  13. 243
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  14. 8
      tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
  15. 6
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  16. 21
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  17. 6
      tests/Avalonia.UnitTests/TestServices.cs
  18. BIN
      tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
  19. BIN
      tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

33
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -310,7 +310,7 @@ namespace Avalonia.Controls.Presenters
var top = 0d;
var left = 0.0;
var (_, textHeight) = TextLayout.Size;
var textHeight = TextLayout.Bounds.Height;
if (Bounds.Height < textHeight)
{
@ -498,25 +498,31 @@ namespace Avalonia.Controls.Presenters
protected override Size MeasureOverride(Size availableSize)
{
_constraint = availableSize;
_textLayout = null;
InvalidateArrange();
var scale = LayoutHelper.GetLayoutScale(this);
_constraint = availableSize;
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, scale);
return TextLayout.Size;
return new Size(measuredSize.Width, measuredSize.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (!double.IsInfinity(_constraint.Width))
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{
return base.ArrangeOverride(finalSize);
return finalSize;
}
_constraint = finalSize;
_textLayout = null;
return base.ArrangeOverride(finalSize);
return finalSize;
}
private int CoerceCaretIndex(int value)
@ -615,11 +621,11 @@ namespace Avalonia.Controls.Presenters
CaretChanged();
}
public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
public CharacterHit GetNextCharacterHit(LogicalDirection direction = LogicalDirection.Forward)
{
if (Text is null)
{
return;
return default;
}
if (FlowDirection == FlowDirection.RightToLeft)
@ -636,7 +642,7 @@ namespace Avalonia.Controls.Presenters
if (lineIndex < 0)
{
return;
return default;
}
if (direction == LogicalDirection.Forward)
@ -697,6 +703,13 @@ namespace Avalonia.Controls.Presenters
}
}
return characterHit;
}
public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
{
var characterHit = GetNextCharacterHit(direction);
UpdateCaret(characterHit);
_navigationPosition = _caretBounds.Position;

2
src/Avalonia.Controls/Primitives/AccessText.cs

@ -78,7 +78,7 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
protected override TextLayout? CreateTextLayout(Size constraint, string? text)
protected override TextLayout CreateTextLayout(Size constraint, string? text)
{
return base.CreateTextLayout(constraint, StripAccessKey(text));
}

63
src/Avalonia.Controls/TextBlock.cs

@ -1,9 +1,11 @@
using System;
using System.Reactive.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Layout;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
@ -146,7 +148,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
public TextLayout? TextLayout
public TextLayout TextLayout
{
get
{
@ -399,25 +401,20 @@ namespace Avalonia.Controls
context.FillRectangle(background, new Rect(Bounds.Size));
}
if (TextLayout is null)
{
return;
}
var padding = Padding;
var top = padding.Top;
var textSize = TextLayout.Size;
var textHeight = TextLayout.Bounds.Height;
if (Bounds.Height < textSize.Height)
if (Bounds.Height < textHeight)
{
switch (VerticalAlignment)
{
case VerticalAlignment.Center:
top += (Bounds.Height - textSize.Height) / 2;
top += (Bounds.Height - textHeight) / 2;
break;
case VerticalAlignment.Bottom:
top += (Bounds.Height - textSize.Height);
top += (Bounds.Height - textHeight);
break;
}
}
@ -431,13 +428,8 @@ namespace Avalonia.Controls
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout? CreateTextLayout(Size constraint, string? text)
protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
{
if (constraint == Size.Empty)
{
return null;
}
return new TextLayout(
text ?? string.Empty,
new Typeface(FontFamily, FontStyle, FontWeight),
@ -464,32 +456,35 @@ namespace Avalonia.Controls
InvalidateMeasure();
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns>
protected override Size MeasureOverride(Size availableSize)
{
if (string.IsNullOrEmpty(Text))
{
return new Size();
}
var padding = Padding;
_constraint = availableSize.Deflate(padding);
_textLayout = null;
availableSize = availableSize.Deflate(padding);
InvalidateArrange();
var scale = LayoutHelper.GetLayoutScale(this);
if (_constraint != availableSize)
{
_constraint = availableSize;
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, scale);
InvalidateTextLayout();
}
return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding);
}
var measuredSize = TextLayout?.Size ?? Size.Empty;
protected override Size ArrangeOverride(Size finalSize)
{
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{
return finalSize;
}
_constraint = finalSize;
_textLayout = null;
return measuredSize.Inflate(padding);
return finalSize;
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;

120
src/Avalonia.Controls/TextBox.cs

@ -880,21 +880,27 @@ namespace Avalonia.Controls
}
else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection))
{
SelectionStart = caretIndex;
MoveHome(true);
SelectionEnd = _presenter.CaretIndex;
movement = true;
selection = true;
handled = true;
}
else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection))
{
SelectionStart = caretIndex;
MoveEnd(true);
SelectionEnd = _presenter.CaretIndex;
movement = true;
selection = true;
handled = true;
}
else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection))
{
SelectionStart = caretIndex;
MoveHome(false);
SelectionEnd = _presenter.CaretIndex;
movement = true;
selection = true;
handled = true;
@ -902,7 +908,9 @@ namespace Avalonia.Controls
}
else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection))
{
SelectionStart = caretIndex;
MoveEnd(false);
SelectionEnd = _presenter.CaretIndex;
movement = true;
selection = true;
handled = true;
@ -979,32 +987,17 @@ namespace Avalonia.Controls
if (!DeleteSelection() && caretIndex > 0)
{
var removedCharacters = 0;
// \r\n needs special treatment here
if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r')
{
removedCharacters = 2;
}
else
{
Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters);
}
if (removedCharacters == 0)
{
return;
}
_presenter.MoveCaretHorizontal(LogicalDirection.Backward);
var removedCharacters = Math.Max(0, caretIndex - _presenter.CaretIndex);
var length = Math.Max(0, caretIndex - removedCharacters);
SetTextInternal(text.Substring(0, length) +
text.Substring(caretIndex));
CaretIndex = caretIndex - removedCharacters;
ClearSelection();
}
SnapshotUndoRedo();
handled = true;
break;
@ -1019,14 +1012,13 @@ namespace Avalonia.Controls
if (!DeleteSelection() && caretIndex < text.Length)
{
_presenter.MoveCaretHorizontal();
var characterHit = _presenter.GetNextCharacterHit();
var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex);
var removedCharacters = Math.Max(0,
characterHit.FirstCharacterIndex + characterHit.TrailingLength - caretIndex);
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
CaretIndex = caretIndex;
}
SnapshotUndoRedo();
@ -1077,6 +1069,8 @@ namespace Avalonia.Controls
{
e.Handled = true;
}
CaretIndex = _presenter.CaretIndex;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
@ -1261,18 +1255,18 @@ namespace Avalonia.Controls
}
else
{
int offset;
if (direction > 0)
{
var offset = StringUtils.NextWord(text, selectionStart) - selectionStart;
CaretIndex += offset;
offset = StringUtils.NextWord(text, selectionStart) - selectionStart;
}
else
{
var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart;
CaretIndex += offset;
offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart;
}
SelectionEnd = CaretIndex + offset;
}
}
@ -1283,32 +1277,20 @@ namespace Avalonia.Controls
return;
}
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
if (document)
{
caretIndex = 0;
_presenter.MoveCaretToTextPosition(0);
}
else
{
var lines = _presenter.TextLayout.TextLines;
var pos = 0;
var textLines = _presenter.TextLayout.TextLines;
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, true);
var textLine = textLines[lineIndex];
foreach (var line in lines)
{
if (pos + line.TextRange.Length > caretIndex || pos + line.TextRange.Length == text.Length)
{
break;
}
pos += line.TextRange.Length;
}
caretIndex = pos;
_presenter.MoveCaretToTextPosition(textLine.TextRange.Start);
}
CaretIndex = caretIndex;
}
private void MoveEnd(bool document)
@ -1323,36 +1305,16 @@ namespace Avalonia.Controls
if (document)
{
caretIndex = text.Length;
_presenter.MoveCaretToTextPosition(text.Length, true);
}
else
{
var lines = _presenter.TextLayout.TextLines;
var pos = 0;
foreach (var line in lines)
{
pos += line.TextRange.Length;
if (pos > caretIndex)
{
if (pos < text.Length)
{
--pos;
if (pos > 0 && text[pos - 1] == '\r' && text[pos] == '\n')
{
--pos;
}
}
break;
}
}
var textLines = _presenter.TextLayout.TextLines;
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
var textLine = textLines[lineIndex];
caretIndex = pos;
_presenter.MoveCaretToTextPosition(textLine.TextRange.Start + textLine.TextRange.Length, true);
}
CaretIndex = caretIndex;
}
/// <summary>
@ -1432,15 +1394,25 @@ namespace Avalonia.Controls
private void SetSelectionForControlBackspace()
{
SelectionStart = CaretIndex;
MoveHorizontal(-1, true, false);
SelectionEnd = CaretIndex;
}
private void SetSelectionForControlDelete()
{
if (_text == null || _presenter == null)
{
return;
}
SelectionStart = CaretIndex;
MoveHorizontal(1, true, false);
SelectionEnd = CaretIndex;
if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ')
{
SelectionEnd++;
}
}
private void UpdatePseudoclasses()

16
src/Avalonia.Controls/Utils/StringUtils.cs

@ -150,17 +150,23 @@ namespace Avalonia.Controls.Utils
return cursor;
}
CharClass cc = GetCharClass(text[cursor]);
i = cursor;
// skip over the word, punctuation, or run of whitespace
while (i < cr && GetCharClass(text[i]) == cc)
// skip any whitespace after the word/punct
while (i < cr && char.IsWhiteSpace(text[i]))
{
i++;
}
// skip any whitespace after the word/punct
while (i < cr && char.IsWhiteSpace(text[i]))
if (i >= cr)
{
return i;
}
var cc = GetCharClass(text[i]);
// skip over the word, punctuation, or run of whitespace
while (i < cr && GetCharClass(text[i]) == cc)
{
i++;
}

3
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -71,6 +71,7 @@ MembersMustExist : Member 'protected System.Boolean Avalonia.Media.TextFormattin
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.IBrush, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Media.TextTrimming, Avalonia.Media.TextDecorationCollection, System.Double, System.Double, System.Double, System.Int32, System.Collections.Generic.IReadOnlyList<Avalonia.Utilities.ValueSpan<Avalonia.Media.TextFormatting.TextRunProperties>>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLayout.Size.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract.
@ -145,4 +146,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphR
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable<System.Int32>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice<T>..ctor(System.ReadOnlyMemory<T>, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
Total Issues: 146
Total Issues: 147

9
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -391,8 +391,13 @@ namespace Avalonia.Media
var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
return nextCharacterHit == characterHit ?
characterHit :
if (characterHit == nextCharacterHit)
{
return characterHit;
}
return characterHit.TrailingLength > 0 ?
nextCharacterHit :
new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}

7
src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs

@ -105,17 +105,12 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
var glyphCount = FindGlyphIndex(Text.Start + length);
if (Text.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
if (Text.Length == glyphCount)
{
return new SplitResult<ShapedBuffer>(this, null);
}
var glyphCount = FindGlyphIndex(Text.Start + length);
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);

10
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@ -138,16 +139,13 @@ namespace Avalonia.Media.TextFormatting
Reverse();
}
#if DEBUG
if(length == 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero.");
}
if(length == ShapedBuffer.Length)
{
return new SplitResult<ShapedTextCharacters>(this, null);
}
#endif
var splitBuffer = ShapedBuffer.Split(length);
var first = new ShapedTextCharacters(splitBuffer.First, Properties);

11
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -369,7 +369,9 @@ namespace Avalonia.Media.TextFormatting
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
return lastCluster - textRange.Start;
var measuredLength = lastCluster - textRange.Start;
return measuredLength == 0 ? 1 : measuredLength;
}
lastCluster = glyphInfo.GlyphCluster;
@ -394,7 +396,7 @@ namespace Avalonia.Media.TextFormatting
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
var currentLength = 0;
@ -506,11 +508,6 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (measuredLength == 0)
{
measuredLength = 1;
}
var splitResult = SplitShapedRuns(textRuns, measuredLength);
textRange = new TextRange(textRange.Start, measuredLength);

62
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<TextLine> TextLines { get; private set; }
/// <summary>
/// Gets the size of the layout.
/// Gets the bounds of the layout.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public Size Size { get; private set; }
public Rect Bounds { get; private set; }
/// <summary>
/// Draws the text layout.
@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting
var lineX = lastLine.Width;
var lineY = Size.Height - lastLine.Height;
var lineY = Bounds.Bottom - lastLine.Height;
return new Rect(lineX, lineY, 0, lastLine.Height);
}
@ -463,7 +463,7 @@ namespace Avalonia.Media.TextFormatting
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Size.Height;
y > Bounds.Bottom;
if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length)
{
@ -505,17 +505,23 @@ namespace Avalonia.Media.TextFormatting
/// Updates the current bounds.
/// </summary>
/// <param name="textLine">The text line.</param>
/// <param name="left">The current left.</param>
/// <param name="width">The current width.</param>
/// <param name="height">The current height.</param>
private static void UpdateBounds(TextLine textLine, ref double width, ref double height)
private static void UpdateBounds(TextLine textLine,ref double left, ref double width, ref double height)
{
var lineWidth = textLine.WidthIncludingTrailingWhitespace + textLine.Start * 2;
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
if (width < lineWidth)
{
width = lineWidth;
}
if (left > textLine.Start)
{
left = textLine.Start;
}
height += textLine.Height;
}
@ -548,14 +554,14 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = CreateEmptyTextLine(0);
Size = new Size(0, textLine.Height);
Bounds = new Rect(0,0,0, textLine.Height);
return new List<TextLine> { textLine };
}
var textLines = new List<TextLine>();
double width = 0.0, height = 0.0;
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
var currentPosition = 0;
@ -569,23 +575,27 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
currentPosition += textLine.TextRange.Length;
#if DEBUG
if (textLine.TextRange.Length == 0)
{
throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
}
#endif
if (textLines.Count > 0)
currentPosition += textLine.TextRange.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
{
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
height + textLine.Height > MaxHeight)
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
textLines[textLines.Count - 1] = collapsedLine;
}
break;
textLines[textLines.Count - 1] = collapsedLine;
}
break;
}
var hasOverflowed = textLine.HasOverflowed;
@ -597,10 +607,16 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine);
UpdateBounds(textLine, ref width, ref height);
UpdateBounds(textLine,ref left, ref width, ref height);
previousLine = textLine;
//Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines)
{
break;
}
if (currentPosition != _text.Length || textLine.NewLineLength <= 0)
{
continue;
@ -610,10 +626,10 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref width, ref height);
UpdateBounds(emptyTextLine,ref left, ref width, ref height);
}
Size = new Size(width, height);
Bounds = new Rect(left, 0, width, height);
return textLines;
}

25
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@ -112,7 +112,13 @@ namespace Avalonia.Media.TextFormatting
var collapsedLength = 0;
var shapedSymbol = TextFormatterImpl.CreateSymbol(collapsingProperties.Symbol, _paragraphProperties.FlowDirection);
if (collapsingProperties.Width < shapedSymbol.GlyphRun.Size.Width)
{
return new TextLineImpl(new List<ShapedTextCharacters>(0), textRange, _paragraphWidth, _paragraphProperties,
_flowDirection, TextLineBreak, true);
}
var availableWidth = collapsingProperties.Width - shapedSymbol.GlyphRun.Size.Width;
while (runIndex < _textRuns.Count)
@ -155,18 +161,19 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength;
var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength);
var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
shapedTextCharacters.AddRange(splitResult.First);
var shapedTextCharacters = new List<ShapedTextCharacters>(_textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength);
shapedTextCharacters.AddRange(splitResult.First);
SortRuns(shapedTextCharacters);
SortRuns(shapedTextCharacters);
}
shapedTextCharacters.Add(shapedSymbol);
textRange = new TextRange(textRange.Start, collapsedLength);
var textLine = new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
_flowDirection, TextLineBreak, true);

243
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -156,112 +156,127 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Selection_Should_Be_Cleared_On_Recycled_Items()
{
var target = new ListBox
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Template = ListBoxTemplate(),
Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectedIndex = 0,
};
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectedIndex = 0,
};
Prepare(target);
Prepare(target);
// Make sure we're virtualized and first item is selected.
Assert.Equal(10, target.Presenter.Panel.Children.Count);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
// Make sure we're virtualized and first item is selected.
Assert.Equal(10, target.Presenter.Panel.Children.Count);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
// Scroll down a page.
target.Scroll.Offset = new Vector(0, 10);
// Scroll down a page.
target.Scroll.Offset = new Vector(0, 10);
// Make sure recycled item isn't now selected.
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
// Make sure recycled item isn't now selected.
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
}
}
[Fact]
public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport()
{
var target = new ListBox
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Template = ListBoxTemplate(),
Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
SelectedIndex = 0,
};
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
SelectedIndex = 0,
};
Prepare(target);
Prepare(target);
Assert.Equal(new Size(20, 20), target.Scroll.Extent);
Assert.Equal(new Size(100, 10), target.Scroll.Viewport);
Assert.Equal(new Size(20, 20), target.Scroll.Extent);
Assert.Equal(new Size(100, 10), target.Scroll.Viewport);
}
}
[Fact]
public void Containers_Correct_After_Clear_Add_Remove()
{
// Issue #1936
var items = new AvaloniaList<string>(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
var target = new ListBox
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
SelectedIndex = 0,
};
// Issue #1936
var items = new AvaloniaList<string>(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
SelectedIndex = 0,
};
Prepare(target);
Prepare(target);
items.Clear();
items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
items.Remove("Item 2");
items.Clear();
items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
items.Remove("Item 2");
Assert.Equal(
items,
target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content));
Assert.Equal(
items,
target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content));
}
}
[Fact]
public void Toggle_Selection_Should_Update_Containers()
{
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Template = ListBoxTemplate(),
Items = items,
SelectionMode = SelectionMode.Toggle,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
};
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
SelectionMode = SelectionMode.Toggle,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
};
Prepare(target);
Prepare(target);
var lbItems = target.GetLogicalChildren().OfType<ListBoxItem>().ToArray();
var lbItems = target.GetLogicalChildren().OfType<ListBoxItem>().ToArray();
var item = lbItems[0];
var item = lbItems[0];
Assert.Equal(false, item.IsSelected);
Assert.Equal(false, item.IsSelected);
RaisePressedEvent(target, item, MouseButton.Left);
RaisePressedEvent(target, item, MouseButton.Left);
Assert.Equal(true, item.IsSelected);
Assert.Equal(true, item.IsSelected);
RaisePressedEvent(target, item, MouseButton.Left);
RaisePressedEvent(target, item, MouseButton.Left);
Assert.Equal(false, item.IsSelected);
Assert.Equal(false, item.IsSelected);
}
}
[Fact]
public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection()
{
var items = new AvaloniaList<string>(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
var target = new ListBox
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
};
var items = new AvaloniaList<string>(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
};
Prepare(target);
target.Scroll.Offset = new Vector(0, 1);
Prepare(target);
target.Scroll.Offset = new Vector(0, 1);
items.RemoveRange(0, 11);
items.RemoveRange(0, 11);
}
}
private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
@ -272,35 +287,38 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown()
{
var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray();
var target = new ListBox
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 })
};
var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray();
Prepare(target);
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 })
};
Prepare(target);
var panel = target.Presenter.Panel as IVirtualizingPanel;
var panel = target.Presenter.Panel as IVirtualizingPanel;
var listBoxItems = panel.Children.OfType<ListBoxItem>();
var listBoxItems = panel.Children.OfType<ListBoxItem>();
//virtualization should have created exactly 10 items
Assert.Equal(10, listBoxItems.Count());
Assert.Equal("0", listBoxItems.First().DataContext);
Assert.Equal("9", listBoxItems.Last().DataContext);
//virtualization should have created exactly 10 items
Assert.Equal(10, listBoxItems.Count());
Assert.Equal("0", listBoxItems.First().DataContext);
Assert.Equal("9", listBoxItems.Last().DataContext);
//instead pixeloffset > 0 there could be pretty complex sequence for repro
//it involves add/remove/scroll to end multiple actions
//which i can't find so far :(, but this is the simplest way to add it to unit test
panel.PixelOffset = 1;
//instead pixeloffset > 0 there could be pretty complex sequence for repro
//it involves add/remove/scroll to end multiple actions
//which i can't find so far :(, but this is the simplest way to add it to unit test
panel.PixelOffset = 1;
//here scroll to end -> IndexOutOfRangeException is thrown
target.Scroll.Offset = new Vector(0, 2);
//here scroll to end -> IndexOutOfRangeException is thrown
target.Scroll.Offset = new Vector(0, 2);
Assert.True(true);
Assert.True(true);
}
}
[Fact]
@ -374,41 +392,44 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control()
{
// Issue #3934
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectionMode = SelectionMode.AlwaysSelected,
VirtualizationMode = ItemVirtualizationMode.None,
};
// Issue #3934
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectionMode = SelectionMode.AlwaysSelected,
VirtualizationMode = ItemVirtualizationMode.None,
};
Prepare(target);
Prepare(target);
// First an item that is not index 0 must be selected.
_mouse.Click(target.Presenter.Panel.Children[1]);
Assert.Equal(1, target.Selection.AnchorIndex);
// First an item that is not index 0 must be selected.
_mouse.Click(target.Presenter.Panel.Children[1]);
Assert.Equal(1, target.Selection.AnchorIndex);
// We're going to be clicking on item 9.
var item = (ListBoxItem)target.Presenter.Panel.Children[9];
var raised = 0;
// We're going to be clicking on item 9.
var item = (ListBoxItem)target.Presenter.Panel.Children[9];
var raised = 0;
// Make sure a RequestBringIntoView event is raised for item 9. It won't be handled
// by the ScrollContentPresenter as the item is already visible, so we don't need
// handledEventsToo: true. Issue #3934 failed here because item 0 was being scrolled
// into view due to SelectionMode.AlwaysSelected.
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) =>
{
Assert.Same(item, e.TargetObject);
++raised;
});
// Make sure a RequestBringIntoView event is raised for item 9. It won't be handled
// by the ScrollContentPresenter as the item is already visible, so we don't need
// handledEventsToo: true. Issue #3934 failed here because item 0 was being scrolled
// into view due to SelectionMode.AlwaysSelected.
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) =>
{
Assert.Same(item, e.TargetObject);
++raised;
});
// Click item 9.
_mouse.Click(item);
// Click item 9.
_mouse.Click(item);
Assert.Equal(1, raised);
Assert.Equal(1, raised);
}
}
[Fact]

8
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@ -100,8 +100,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var fmt = Create(input, fontSize);
Assert.Equal(expWidth, fmt.Size.Width, 2);
Assert.Equal(expHeight, fmt.Size.Height, 2);
Assert.Equal(expWidth, fmt.Bounds.Width, 2);
Assert.Equal(expHeight, fmt.Bounds.Height, 2);
}
[Theory]
@ -279,7 +279,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = new Rect(t.Size);
var textRect = t.Bounds;
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
c.DrawRectangle(Brushes.Yellow, null, rect);
@ -311,7 +311,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = new Rect(t.Size);
var textRect = t.Bounds;
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
var rotate = Matrix.CreateTranslation(-100, -100) *

6
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -588,7 +588,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, layout.TextLines.Count);
Assert.Equal(lineHeight, layout.Size.Height);
Assert.Equal(lineHeight, layout.Bounds.Height);
}
}
@ -716,7 +716,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var selectedRect = rects[0];
Assert.Equal(selectedText.Size.Width, selectedRect.Width);
Assert.Equal(selectedText.Bounds.Width, selectedRect.Width);
}
}
@ -832,7 +832,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
12,
Brushes.Black);
Assert.True(layout.Size.Height > 0);
Assert.True(layout.Bounds.Height > 0);
}
}

21
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -360,10 +360,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234\u2026")]
[InlineData("01234 01234", 58, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
[InlineData("01234 01234", 58, TextCollapsingStyle.TrailingWord, "01234\u2026")]
[InlineData("01234", 9, TextCollapsingStyle.TrailingCharacter, "\u2026")]
[InlineData("01234", 2, TextCollapsingStyle.TrailingCharacter, "")]
[Theory]
public void Should_Collapse_Line(string text, int numberOfCharacters, TextCollapsingStyle style, string expected)
public void Should_Collapse_Line(string text, double width, TextCollapsingStyle style, string expected)
{
using (Start())
{
@ -379,19 +381,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.False(textLine.HasCollapsed);
var glyphTypeface = Typeface.Default.GlyphTypeface;
var scale = defaultProperties.FontRenderingEmSize / glyphTypeface.DesignEmHeight;
var width = 1.0;
for (var i = 0; i < numberOfCharacters; i++)
{
var glyph = glyphTypeface.GetGlyph(text[i]);
width += glyphTypeface.GetGlyphAdvance(glyph) * scale;
}
TextCollapsingProperties collapsingProperties;
if (style == TextCollapsingStyle.TrailingCharacter)

6
tests/Avalonia.UnitTests/TestServices.cs

@ -53,7 +53,11 @@ namespace Avalonia.UnitTests
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: new KeyboardNavigationHandler(),
inputManager: new InputManager());
inputManager: new InputManager(),
assetLoader: new AssetLoader(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl());
public static readonly TestServices RealStyler = new TestServices(
styler: new Styler());

BIN
tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 768 B

BIN
tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 532 B

Loading…
Cancel
Save