Browse Source

Don't build the selected text just to check whether a selection exists (#22100)

* perf(text): test the selection by index instead of building it

UpdateCommandStates only needs to know whether the selection is empty, but it
called GetSelection, which allocates a substring on every selection change. Test
the indices instead, so a selection change no longer allocates.

Requested by @Gillibald in #21492.

* perf(text): read the selection properties once in HasSelection
pull/22115/head
Nathan Nguyen 1 month ago
committed by GitHub
parent
commit
a8b1a6b55a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 23
      src/Avalonia.Controls/SelectableTextBlock.cs
  2. 25
      src/Avalonia.Controls/TextBox.cs
  3. 45
      tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs

23
src/Avalonia.Controls/SelectableTextBlock.cs

@ -539,9 +539,28 @@ namespace Avalonia.Controls
private void UpdateCommandStates() private void UpdateCommandStates()
{ {
var text = GetSelection(); CanCopy = HasSelection();
}
/// <summary>
/// Reports the same emptiness conditions as <see cref="GetSelection"/>, without building
/// the selected string.
/// </summary>
private bool HasSelection()
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
if (start == end)
{
return false;
}
var textLength = (HasComplexContent ? Inlines?.Text : Text)?.Length ?? 0;
CanCopy = !string.IsNullOrEmpty(text); return textLength > 0 && end <= textLength;
} }
private string GetSelection() private string GetSelection()

25
src/Avalonia.Controls/TextBox.cs

@ -1112,10 +1112,9 @@ namespace Avalonia.Controls
private void UpdateCommandStates() private void UpdateCommandStates()
{ {
var text = GetSelection(); var hasSelection = HasSelection();
var isSelectionNullOrEmpty = string.IsNullOrEmpty(text); CanCopy = !IsPasswordBox && hasSelection;
CanCopy = !IsPasswordBox && !isSelectionNullOrEmpty; CanCut = !IsPasswordBox && hasSelection && !IsReadOnly;
CanCut = !IsPasswordBox && !isSelectionNullOrEmpty && !IsReadOnly;
CanPaste = !IsReadOnly; CanPaste = !IsReadOnly;
} }
@ -2450,6 +2449,24 @@ namespace Avalonia.Controls
return false; return false;
} }
/// <summary>
/// Reports the same emptiness conditions as <see cref="GetSelection"/>, without building
/// the selected string.
/// </summary>
private bool HasSelection()
{
var (start, end) = GetSelectionRange();
if (start == end)
{
return false;
}
var textLength = Text?.Length ?? 0;
return textLength > 0 && end <= textLength;
}
private string GetSelection() private string GetSelection()
{ {
var text = Text; var text = Text;

45
tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs

@ -273,6 +273,51 @@ namespace Avalonia.Controls.UnitTests
Assert.IsType<InvalidOperationException>(Record.Exception(syncContext.ExecutePostedCallbacks)); Assert.IsType<InvalidOperationException>(Record.Exception(syncContext.ExecutePostedCallbacks));
} }
[Theory]
[InlineData(2, 2, false)]
[InlineData(1, 3, true)]
[InlineData(3, 1, true)]
[InlineData(0, 4, true)]
public void CanCopy_Tracks_Whether_Selection_Covers_Any_Character(int start, int end, bool expected)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock { Text = "abcd" };
target.Measure(Size.Infinity);
target.SelectionStart = start;
target.SelectionEnd = end;
Assert.Equal(expected, target.CanCopy);
}
}
[Fact]
public void CanCopy_Tracks_Selection_Over_Inlines()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock();
target.Inlines!.Add(new Run("foo"));
target.Inlines!.Add(new Run("bar"));
target.Measure(Size.Infinity);
Assert.False(target.CanCopy);
target.SelectionStart = 2;
target.SelectionEnd = 5;
Assert.True(target.CanCopy);
target.ClearSelection();
Assert.False(target.CanCopy);
}
}
private static TestServices ClipboardServices private static TestServices ClipboardServices
=> TestServices.MockThreadingInterface.With( => TestServices.MockThreadingInterface.With(
assetLoader: new StandardAssetLoader(), assetLoader: new StandardAssetLoader(),

Loading…
Cancel
Save