diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 8629af5243..05ae4e11f3 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/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;
diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs
index a6976721b1..1e0f26907d 100644
--- a/src/Avalonia.Controls/Primitives/AccessText.cs
+++ b/src/Avalonia.Controls/Primitives/AccessText.cs
@@ -78,7 +78,7 @@ namespace Avalonia.Controls.Primitives
}
///
- protected override TextLayout? CreateTextLayout(Size constraint, string? text)
+ protected override TextLayout CreateTextLayout(Size constraint, string? text)
{
return base.CreateTextLayout(constraint, StripAccessKey(text));
}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index ed012bd8b1..e41328b79a 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/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
///
/// Gets the used to render the text.
///
- 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
/// The constraint of the text.
/// The text to format.
/// A object.
- 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();
}
- ///
- /// Measures the control.
- ///
- /// The available size for the control.
- /// The desired size.
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;
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 4ec0c4c5e1..69e9ca5b92 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/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;
}
///
@@ -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()
diff --git a/src/Avalonia.Controls/Utils/StringUtils.cs b/src/Avalonia.Controls/Utils/StringUtils.cs
index 53937003c8..b2e56434b2 100644
--- a/src/Avalonia.Controls/Utils/StringUtils.cs
+++ b/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++;
}
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index 828ea1f184..70fcb6bc00 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/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>)' 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, 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)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 146
+Total Issues: 147
diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs
index ef5ffb8d78..ec270d796a 100644
--- a/src/Avalonia.Visuals/Media/GlyphRun.cs
+++ b/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);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs
index ee38cf39e0..47a6334e39 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs
@@ -105,17 +105,12 @@ namespace Avalonia.Media.TextFormatting
/// The split result.
internal SplitResult Split(int length)
{
- var glyphCount = FindGlyphIndex(Text.Start + length);
-
if (Text.Length == length)
{
return new SplitResult(this, null);
}
- if (Text.Length == glyphCount)
- {
- return new SplitResult(this, null);
- }
+ var glyphCount = FindGlyphIndex(Text.Start + length);
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index 96b3857098..88ca596d2d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/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(this, null);
- }
-
+#endif
+
var splitBuffer = ShapedBuffer.Split(length);
var first = new ShapedTextCharacters(splitBuffer.First, Properties);
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 101f273798..d687fb25ed 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/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);
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 64f0eaab53..8494a7cbd5 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList TextLines { get; private set; }
///
- /// Gets the size of the layout.
+ /// Gets the bounds of the layout.
///
///
/// The bounds.
///
- public Size Size { get; private set; }
+ public Rect Bounds { get; private set; }
///
/// 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.
///
/// The text line.
+ /// The current left.
/// The current width.
/// The current height.
- 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 };
}
var textLines = new List();
- 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;
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index 53e44de779..ff2bbf53da 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/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(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(splitResult.First.Count + 1);
-
- shapedTextCharacters.AddRange(splitResult.First);
+ var shapedTextCharacters = new List(_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);
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
index aa63e18691..e87990ebb1 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
+++ b/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((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((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((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((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(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
- var target = new ListBox
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- Template = ListBoxTemplate(),
- Items = items,
- ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }),
- SelectedIndex = 0,
- };
+ // Issue #1936
+ var items = new AvaloniaList(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ Items = items,
+ ItemTemplate = new FuncDataTemplate((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().Select(x => (string)x.Content));
+ Assert.Equal(
+ items,
+ target.Presenter.Panel.Children.Cast().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((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((x, _) => new TextBlock { Height = 10 })
+ };
- Prepare(target);
+ Prepare(target);
- var lbItems = target.GetLogicalChildren().OfType().ToArray();
+ var lbItems = target.GetLogicalChildren().OfType().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(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
- var target = new ListBox
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- Template = ListBoxTemplate(),
- Items = items,
- ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 })
- };
+ var items = new AvaloniaList(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ Items = items,
+ ItemTemplate = new FuncDataTemplate((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((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((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();
+ var listBoxItems = panel.Children.OfType();
- //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((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((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]
diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
index 981ae0d0a4..6ed4ba0d4a 100644
--- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/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) *
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index 595bcd4009..d331def414 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/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);
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 8cb010f42b..d1a8f175e7 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/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)
diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs
index 2c4ae06fb2..1d55b77aab 100644
--- a/tests/Avalonia.UnitTests/TestServices.cs
+++ b/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());
diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
index c5a0a14e52..edd4dfd263 100644
Binary files a/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png and b/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png differ
diff --git a/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
index c5a0a14e52..a76c6a5b2a 100644
Binary files a/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png and b/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png differ