A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

382 lines
14 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class SelectableTextBlockTests : ScopedTestBase
{
// Content: Run("foo") + InlineUIContainer + Run("bar")
// Inlines.Text after fix: "foo\uFFFCbar" (indices 0-6)
// 0='f', 1='o', 2='o', 3='\uFFFC' (embedded control), 4='b', 5='a', 6='r'
[Theory]
// Entirely before InlineUIContainer
[InlineData(0, 3, "foo")]
// Exactly the InlineUIContainer character
[InlineData(3, 4, "\uFFFC")]
// Up to and including InlineUIContainer (fencepost: last char before "bar")
[InlineData(0, 4, "foo\uFFFC")]
// Starting exactly after InlineUIContainer (fencepost: first char of "bar")
[InlineData(4, 7, "bar")]
// InlineUIContainer through end
[InlineData(3, 7, "\uFFFCbar")]
// Spanning InlineUIContainer (one char either side)
[InlineData(2, 5, "o\uFFFCb")]
// Entire content
[InlineData(0, 7, "foo\uFFFCbar")]
public void Selection_With_InlineUIContainer_Returns_Correct_Text(int start, int end, string expected)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock();
target.Inlines!.Add(new Run("foo"));
target.Inlines!.Add(new InlineUIContainer(new Border()));
target.Inlines!.Add(new Run("bar"));
target.Measure(Size.Infinity);
// SelectionStart/End values correspond to TextLayout character positions.
// EmbeddedControlRun occupies 1 position (TextRun.DefaultTextSourceLength),
// and Inlines.Text now has a matching U+FFFC placeholder, so they stay in sync.
target.SelectionStart = start;
target.SelectionEnd = end;
Assert.Equal(expected, target.SelectedText);
}
}
[Theory]
[InlineData(TextAlignment.Center)]
[InlineData(TextAlignment.Right)]
public void Dragging_Selection_Should_Reach_End_Of_Text_When_Text_Is_Aligned(TextAlignment textAlignment)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock
{
Width = 200,
Text = "Aligned text",
TextAlignment = textAlignment
};
var root = new TestRoot(target)
{
ClientSize = new Size(300, 100)
};
root.Measure(root.ClientSize);
root.Arrange(new Rect(root.ClientSize));
root.ExecuteInitialLayoutPass();
var firstCharacterBounds = target.TextLayout.HitTestTextPosition(0);
var lastCharacterBounds = target.TextLayout.HitTestTextPosition(target.Text!.Length - 1);
var mouse = new MouseTestHelper();
var startPoint = new Point(
firstCharacterBounds.X + firstCharacterBounds.Width / 2,
firstCharacterBounds.Y + firstCharacterBounds.Height / 2);
var endPoint = new Point(
Math.Min(target.Bounds.Width - 1, lastCharacterBounds.Right + 10),
lastCharacterBounds.Y + lastCharacterBounds.Height / 2);
mouse.Down(target, position: target.TranslatePoint(startPoint, root));
mouse.Move(target, position: target.TranslatePoint(endPoint, root).GetValueOrDefault());
Assert.Equal(target.Text!.Length, Math.Max(target.SelectionStart, target.SelectionEnd));
}
}
[Fact]
public void SelectionForeground_Should_Not_Reset_Run_Typeface_And_Style()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock
{
SelectionForegroundBrush = Brushes.Red
};
var run = new Run("Hello")
{
FontWeight = FontWeight.Bold,
FontStyle = FontStyle.Italic,
FontSize = 20
};
target.Inlines!.Add(run);
target.Measure(Size.Infinity);
target.SelectionStart = 0;
target.SelectionEnd = run.Text!.Length;
target.Measure(Size.Infinity);
var textLayout = target.TextLayout;
Assert.NotNull(textLayout);
var textRuns = textLayout.TextLines
.SelectMany(l => l.TextRuns)
.OfType<ShapedTextRun>()
.ToList();
Assert.NotEmpty(textRuns);
var selectedRun = textRuns[0];
var props = selectedRun.Properties;
Assert.Equal(FontWeight.Bold, props.Typeface.Weight);
Assert.Equal(FontStyle.Italic, props.Typeface.Style);
Assert.Same(target.SelectionForegroundBrush, props.ForegroundBrush);
}
}
[Fact]
public void Inlines_Changes_Should_Update_Selection()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock();
target.Inlines!.Add(new Run("foo"));
target.SelectionEnd = 3;
var selectedTextChanged = false;
target.PropertyChanged += (_, e) =>
{
if (e.Property == SelectableTextBlock.SelectedTextProperty)
{
selectedTextChanged = true;
}
};
target.Inlines.Add(new Run("bar"));
Assert.True(selectedTextChanged);
target.SelectionStart = 6;
target.SelectionEnd = 0;
target.Inlines.RemoveAt(1);
Assert.Equal(3, target.SelectionStart);
Assert.Equal(0, target.SelectionEnd);
target.SelectionStart = 0;
target.SelectionEnd = 3;
target.Inlines[0] = new Run("a");
Assert.Equal(0, target.SelectionStart);
Assert.Equal(1, target.SelectionEnd);
Assert.Equal("a", target.SelectedText);
}
}
[Fact]
public void Text_Changes_Should_Update_Selection()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock
{
Text = "foo",
SelectionEnd = 3
};
var selectedTextChanged = false;
target.PropertyChanged += (_, e) =>
{
if (e.Property == SelectableTextBlock.SelectedTextProperty)
{
selectedTextChanged = true;
}
};
target.Text = "a";
Assert.Equal(0, target.SelectionStart);
Assert.Equal(1, target.SelectionEnd);
Assert.True(selectedTextChanged);
}
}
[Fact]
public void CoerceCaretIndex_OnTextChanged()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new SelectableTextBlock
{
Text = "foo",
SelectionStart = 3,
SelectionEnd = 3
};
target.Text = "a";
Assert.Equal(1, target.SelectionStart);
Assert.Equal(1, target.SelectionEnd);
}
}
[Theory]
[InlineData(typeof(TimeoutException))]
[InlineData(typeof(OperationCanceledException))]
[InlineData(typeof(UnauthorizedAccessException))]
[InlineData(typeof(COMException))]
public void Copy_Does_Not_Throw_When_Clipboard_Fails(Type exceptionType)
{
using var app = UnitTestApplication.Start(ClipboardServices);
var clipboardImpl = new ThrowingClipboardImplStub(exceptionType);
var target = CreateSelectableTextBlockInTopLevel(clipboardImpl);
var messages = new List<string>();
using (TestLogSink.Start((_, _, _, message, _) => messages.Add(message)))
{
using var syncContext = UnitTestSynchronizationContext.Begin();
target.Copy();
Assert.Null(Record.Exception(syncContext.ExecutePostedCallbacks));
}
Assert.Equal(1, clipboardImpl.SetDataCount);
Assert.Equal(["Failed to write text to clipboard: {Error}"], messages);
}
[Fact]
public void Copy_Does_Not_Swallow_Unexpected_Exceptions()
{
using var app = UnitTestApplication.Start(ClipboardServices);
var clipboardImpl = new ThrowingClipboardImplStub(typeof(InvalidOperationException));
var target = CreateSelectableTextBlockInTopLevel(clipboardImpl);
using var syncContext = UnitTestSynchronizationContext.Begin();
target.Copy();
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(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new TestFontManager());
private static SelectableTextBlock CreateSelectableTextBlockInTopLevel(IClipboardImpl clipboardImpl)
{
var target = new SelectableTextBlock
{
Text = "abcd",
SelectionStart = 1,
SelectionEnd = 3
};
var impl = new Mock<ITopLevelImpl>();
impl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
impl.Setup(x => x.TryGetFeature(typeof(IClipboard))).Returns(new Clipboard(clipboardImpl));
impl.SetupGet(x => x.RenderScaling).Returns(1);
var topLevel = new TestTopLevel(impl.Object)
{
Template = new FuncControlTemplate<TestTopLevel>((x, scope) =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope)),
Content = target
};
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
Assert.True(target.CanCopy);
return target;
}
[Fact]
public void Should_Shape_Inlines_When_TextLayout_Is_Created_Between_Content_Change_And_Measure()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var target = new SelectableTextBlock { Inlines = new InlineCollection() };
target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000));
target.Inlines = new InlineCollection { new Run("Hello World") };
_ = target.TextLayout;
target.Measure(new Size(1000, 1000));
Assert.True(target.DesiredSize.Width > 0, $"DesiredSize was {target.DesiredSize}");
}
private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl);
}
}