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(),