diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs
index 3c522f6f69..1f84da6a87 100644
--- a/src/Avalonia.Controls/SelectableTextBlock.cs
+++ b/src/Avalonia.Controls/SelectableTextBlock.cs
@@ -539,9 +539,28 @@ namespace Avalonia.Controls
private void UpdateCommandStates()
{
- var text = GetSelection();
+ CanCopy = HasSelection();
+ }
+
+ ///
+ /// Reports the same emptiness conditions as , without building
+ /// the selected string.
+ ///
+ 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()
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 40cf617d0a..64a26eab8a 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/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;
}
+ ///
+ /// Reports the same emptiness conditions as , without building
+ /// the selected string.
+ ///
+ 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;
diff --git a/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs
index 90a2ee5d8b..ddf78fb7c4 100644
--- a/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs
@@ -273,6 +273,51 @@ namespace Avalonia.Controls.UnitTests
Assert.IsType(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(),