Browse Source

Listen to layout changes for ancestor visuals in text selection canvas (#21536)

* listen to layout changes for ancestor visuals in text selection canvas

* add tests
pull/21541/head
Emmanuel Hansen 4 months ago
committed by GitHub
parent
commit
69ecd31530
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 56
      src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
  2. 152
      tests/Avalonia.Controls.UnitTests/TextSelectionCanvasTests.cs

56
src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs

@ -5,9 +5,12 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media.TextFormatting;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.VisualTree;
using static Avalonia.Reactive.Disposable;
namespace Avalonia.Controls.Primitives
{
@ -97,9 +100,16 @@ namespace Avalonia.Controls.Primitives
}
private void LayoutListener_Invalidated(object? sender, EventArgs e)
{
InvalidateMeasure();
}
protected override Size MeasureOverride(Size availableSize)
{
if (ShowHandles)
MoveHandlesToSelection();
return base.MeasureOverride(availableSize);
}
private void Caret_ContextCanceled(object? sender, RoutedEventArgs e)
@ -610,15 +620,48 @@ namespace Avalonia.Controls.Primitives
private TextPresenter? _presenter;
public event EventHandler? Invalidated;
private CompositeDisposable? _disposables;
public void Attach(TextPresenter presenter)
{
if (_presenter != null)
throw new InvalidOperationException("Listener is already attached to a TextPresenter");
_disposables?.Dispose();
_disposables = new CompositeDisposable();
_presenter = presenter;
presenter.SizeChanged += Presenter_SizeChanged;
presenter.EffectiveViewportChanged += Visual_EffectiveViewportChanged;
Visual? current = presenter;
do
{
AttachEvents(current);
current = current?.VisualParent;
}
while (current != null);
}
private void AttachEvents(Visual? visual)
{
if (visual != null)
{
if (visual is Layoutable layoutable)
{
layoutable.EffectiveViewportChanged += Visual_EffectiveViewportChanged;
_disposables?.Add(new AnonymousDisposable(() =>
{
layoutable.EffectiveViewportChanged -= Visual_EffectiveViewportChanged;
}));
}
if (visual is Control control)
{
control.SizeChanged += Visual_SizeChanged;
_disposables?.Add(new AnonymousDisposable(() =>
{
control.SizeChanged -= Visual_SizeChanged;
}));
}
}
}
private void Visual_EffectiveViewportChanged(object? sender, Layout.EffectiveViewportChangedEventArgs e)
@ -626,18 +669,15 @@ namespace Avalonia.Controls.Primitives
OnInvalidated();
}
private void Presenter_SizeChanged(object? sender, SizeChangedEventArgs e)
private void Visual_SizeChanged(object? sender, SizeChangedEventArgs e)
{
OnInvalidated();
}
public void Detach()
{
if (_presenter is { } presenter)
{
presenter.SizeChanged -= Presenter_SizeChanged;
presenter.EffectiveViewportChanged -= Visual_EffectiveViewportChanged;
}
_disposables?.Dispose();
_disposables = null;
_presenter = null;
}

152
tests/Avalonia.Controls.UnitTests/TextSelectionCanvasTests.cs

@ -0,0 +1,152 @@
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Harfbuzz;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class TextSelectionCanvasTests : ScopedTestBase
{
[Fact]
public void Text_Selection_Handle_Moves_With_Scrolling()
{
using (UnitTestApplication.Start(Services))
{
var touchHelper = new TouchTestHelper();
Border rootBorder = new Border()
{
Width = 200,
Height = 600
};
var visualLayerManager = new VisualLayerManager()
{
Child = rootBorder,
EnableTextSelectorLayer = true
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = visualLayerManager
};
var focusedTextBox = CreateTextBox();
var panel = new StackPanel()
{
Orientation = Layout.Orientation.Vertical,
Spacing = 20,
Children =
{
focusedTextBox,
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
CreateTextBox(),
}
};
var scrollViewer = new ScrollViewer
{
Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate),
Content = panel
};
rootBorder.Child = scrollViewer;
topLevel.LayoutManager.ExecuteInitialLayoutPass();
touchHelper.Tap(focusedTextBox);
topLevel.LayoutManager.ExecuteLayoutPass();
var presenter = focusedTextBox.FindDescendantOfType<TextPresenter>()!;
var canvas = presenter.TextSelectionHandleCanvas;
Assert.NotNull(canvas);
var handle = canvas.Children.FirstOrDefault() as TextSelectionHandle;
Assert.NotNull(handle);
Assert.Equal(new Point(25.5, 14.5), handle.GetTopLeft());
scrollViewer.Offset = new Vector(0, 50);
topLevel.LayoutManager.ExecuteLayoutPass();
Assert.Equal(new Point(25.5, -35.5), handle.GetTopLeft());
}
TextBox CreateTextBox()
{
return new TextBox
{
Template = TextBoxTests.CreateTemplate(),
Text = "Test",
Width = 150
};
}
}
private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl)
{
}
static Mock<ITopLevelImpl> CreateMockTopLevelImpl()
{
var topLevel = new Mock<ITopLevelImpl>();
topLevel.Setup(x => x.RenderScaling).Returns(1);
topLevel.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
return topLevel;
}
private static FuncControlTemplate<TestTopLevel> CreateTopLevelTemplate()
{
return new FuncControlTemplate<TestTopLevel>((x, scope) =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope));
}
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<ICursorFactory>(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HarfBuzzTextShaper(),
fontManagerImpl: new TestFontManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
assetLoader: new StandardAssetLoader());
}
}
Loading…
Cancel
Save