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