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 4 weeks 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()
{
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()

25
src/Avalonia.Controls/TextBox.cs

@ -1112,10 +1112,9 @@ namespace Avalonia.Controls
private void UpdateCommandStates()
{
var text = GetSelection();
var isSelectionNullOrEmpty = string.IsNullOrEmpty(text);
CanCopy = !IsPasswordBox && !isSelectionNullOrEmpty;
CanCut = !IsPasswordBox && !isSelectionNullOrEmpty && !IsReadOnly;
var hasSelection = HasSelection();
CanCopy = !IsPasswordBox && hasSelection;
CanCut = !IsPasswordBox && hasSelection && !IsReadOnly;
CanPaste = !IsReadOnly;
}
@ -2450,6 +2449,24 @@ namespace Avalonia.Controls
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()
{
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));
}
[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
=> TestServices.MockThreadingInterface.With(
assetLoader: new StandardAssetLoader(),

Loading…
Cancel
Save