From 1365b64d9158248f071a8c8c43313101590d8979 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Mon, 4 May 2026 11:16:43 +0200 Subject: [PATCH 1/2] Avoid exceptions when getting position from a PointerEventArgs (#21301) Return null when an IPresentationSource can't convert from/to screen coordinates Document exceptions in VisualExtensions PointToClient and PointToScreen Add tests for cross-root pointer event positions --- src/Avalonia.Base/Input/PointerEventArgs.cs | 40 ++++++----- .../Rendering/IPresentationSource.cs | 4 +- src/Avalonia.Base/VisualExtensions.cs | 10 +-- .../PresentationSource.RenderRoot.cs | 4 +- .../Input/MouseDeviceTests.cs | 72 ++++++++++++++++++- tests/Avalonia.RenderTests/TestRenderRoot.cs | 4 +- tests/Avalonia.UnitTests/TestRoot.cs | 4 +- 7 files changed, 106 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 9285e65fa5..ee1b54b792 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -3,14 +3,14 @@ using System.Collections.Generic; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.VisualTree; +using Avalonia.Rendering; namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { - private readonly Visual? _rootVisual; - private readonly Point _rootVisualPosition; + private readonly IPresentationSource? _eventPresentationSource; // the original observer of the event + private readonly Point _presentationSourcePosition; private readonly PointerPointProperties _properties; private readonly Lazy?>? _previousPoints; @@ -24,8 +24,8 @@ namespace Avalonia.Input : base(routedEvent) { Source = source; - _rootVisual = rootVisual; - _rootVisualPosition = rootVisualPosition; + _eventPresentationSource = rootVisual?.PresentationSource; + _presentationSourcePosition = rootVisualPosition; _properties = properties; Pointer = pointer; Timestamp = timestamp; @@ -76,23 +76,25 @@ namespace Avalonia.Input private Point GetPosition(Point pt, Visual? relativeTo) { - if (_rootVisual == null) - return default; if (relativeTo == null) return pt; + if (_eventPresentationSource == null || relativeTo?.PresentationSource?.RootVisual == null) + return default; - // If the visual the user passed in, is not connected to the same visual root - // (i.e. they called it for a control inside a popup. - if (!ReferenceEquals(_rootVisual, relativeTo.VisualRoot) && relativeTo.VisualRoot is { }) + if (relativeTo.PresentationSource != _eventPresentationSource) { - // Convert to absolute screen coordinates. - var screenPt = _rootVisual.PointToScreen(pt); - - // Convert to client co-ordinates of the visual inside the other visual root. - return relativeTo.PointToClient(screenPt); + if (_eventPresentationSource.PointToScreen(pt) is { } screenPt && + relativeTo.PresentationSource.PointToClient(screenPt) is { } targetClientPt) + { + pt = targetClientPt; + } + else + { + return default; + } } - return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; + return relativeTo.PresentationSource.RootVisual.TranslatePoint(pt, relativeTo) ?? default; } /// @@ -100,7 +102,7 @@ namespace Avalonia.Input /// /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// The pointer position in the control's coordinates. - public Point GetPosition(Visual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); + public Point GetPosition(Visual? relativeTo) => GetPosition(_presentationSourcePosition, relativeTo); /// /// Returns the PointerPoint associated with the current event @@ -117,7 +119,7 @@ namespace Avalonia.Input /// public IReadOnlyList GetIntermediatePoints(Visual? relativeTo) { - var previousPoints = _previousPoints?.Value; + var previousPoints = _previousPoints?.Value; if (previousPoints == null || previousPoints.Count == 0) return new[] { GetCurrentPoint(relativeTo) }; var points = new PointerPoint[previousPoints.Count + 1]; @@ -145,7 +147,7 @@ namespace Avalonia.Input /// public PointerPointProperties Properties => _properties; } - + public enum MouseButton { None, diff --git a/src/Avalonia.Base/Rendering/IPresentationSource.cs b/src/Avalonia.Base/Rendering/IPresentationSource.cs index bf5beebe49..ff9da11afa 100644 --- a/src/Avalonia.Base/Rendering/IPresentationSource.cs +++ b/src/Avalonia.Base/Rendering/IPresentationSource.cs @@ -41,6 +41,6 @@ public interface IPresentationSource /// internal Size ClientSize { get; } - internal PixelPoint PointToScreen(Point point); - internal Point PointToClient(PixelPoint point); + internal PixelPoint? PointToScreen(Point point); + internal Point? PointToClient(PixelPoint point); } diff --git a/src/Avalonia.Base/VisualExtensions.cs b/src/Avalonia.Base/VisualExtensions.cs index 3df2eca039..fa4236bf46 100644 --- a/src/Avalonia.Base/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualExtensions.cs @@ -14,12 +14,13 @@ namespace Avalonia /// The visual. /// The point in screen coordinates. /// The point in client coordinates. + /// Thown when does not belong to a visual tree. public static Point PointToClient(this Visual visual, PixelPoint point) { var source = visual.PresentationSource; var root = source?.RootVisual ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var rootPoint = source.PointToClient(point); + throw new ArgumentException("Visual does not belong to a visual tree.", nameof(visual)); + var rootPoint = source.PointToClient(point) ?? default; return root.TranslatePoint(rootPoint, visual)!.Value; } @@ -29,13 +30,14 @@ namespace Avalonia /// The visual. /// The point in client coordinates. /// The point in screen coordinates. + /// Thown when does not belong to a visual tree. public static PixelPoint PointToScreen(this Visual visual, Point point) { var source = visual.PresentationSource; var root = source?.RootVisual ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); + throw new ArgumentException("Visual does not belong to a visual tree.", nameof(visual)); var p = visual.TranslatePoint(point, root); - return source.PointToScreen(p!.Value); + return source.PointToScreen(p!.Value) ?? default; } /// diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs index 2a9e833a48..e5fb12eba4 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs @@ -23,9 +23,9 @@ internal partial class PresentationSource _pointerOverPreProcessor?.SceneInvalidated(sceneInvalidatedEventArgs.DirtyRect); } - public PixelPoint PointToScreen(Point point) => PlatformImpl?.PointToScreen(point) ?? default; + public PixelPoint? PointToScreen(Point point) => PlatformImpl?.PointToScreen(point); - public Point PointToClient(PixelPoint point) => PlatformImpl?.PointToClient(point) ?? default; + public Point? PointToClient(PixelPoint point) => PlatformImpl?.PointToClient(point); private void HandleScalingChanged(double scaling) => RenderScaling = LayoutHelper.ValidateScaling(scaling); diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs index 1ec6ec2be5..f8bb8c3ed2 100644 --- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs @@ -1,4 +1,6 @@ -using Avalonia.Controls; +using System; +using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media; @@ -128,6 +130,74 @@ namespace Avalonia.Base.UnitTests.Input Assert.Equal(new Point(1, 11), result); } + private IDisposable SetupCrossTreePositionRequest(PixelPoint topLevelPosition, out PointerEventArgs pointerEvent, out Control elementA, out Control elementB) + { + var app = UnitTestApplication.Start(new TestServices( + inputManager: new InputManager(), + renderInterface: new HeadlessPlatformRenderInterface())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl1 = CreateTopLevelImplMock(); + // Mocked position: topLevelPosition + impl1.Setup(w => w.PointToScreen(default)).Returns(p => (PixelPoint.FromPoint(p, 1) + topLevelPosition)); + + elementA = new Border(); + PointerEventArgs? moveEventArgs = null; + + elementA.PointerMoved += (s, e) => moveEventArgs = e; + var root1 = CreateInputRoot(impl1.Object, elementA, renderer.Object); + + SetMove(deviceMock, root1.InputRoot, elementA); + impl1.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root1)); + + Assert.NotNull(moveEventArgs); + pointerEvent = moveEventArgs; + + var impl2 = CreateTopLevelImplMock(); + // Mocked position: topLevelPosition * 2 + impl2.Setup(w => w.PointToClient(default)).Returns(p => (p - topLevelPosition - topLevelPosition).ToPoint(1)); + + elementB = new Border(); + var root2 = CreateInputRoot(impl2.Object, elementB, renderer.Object); + + return app; + } + + [Fact] + public void GetPosition_Should_Support_Cross_Tree_Requests() + { + var topLevelOffset = new PixelPoint(5, 0); + using (SetupCrossTreePositionRequest(topLevelOffset, out var pointerEvent, out _, out var elementB)) + { + Assert.Equal(topLevelOffset.ToPoint(1), pointerEvent.GetPosition(elementB)); + } + } + + [Fact] + public void GetPosition_Should_Return_Default_When_Cross_Tree_Source_Closed() + { + var topLevelOffset = new PixelPoint(5, 0); + using (SetupCrossTreePositionRequest(topLevelOffset, out var pointerEvent, out var elementA, out var elementB)) + { + ((PresentationSource)elementA.PresentationSource!).Dispose(); + + Assert.Equal(default, pointerEvent.GetPosition(elementB)); + } + } + + [Fact] + public void GetPosition_Should_Return_Default_When_Cross_Tree_Target_Closed() + { + var topLevelOffset = new PixelPoint(5, 0); + using (SetupCrossTreePositionRequest(topLevelOffset, out var pointerEvent, out _, out var elementB)) + { + ((PresentationSource)elementB.PresentationSource!).Dispose(); + + Assert.Equal(default, pointerEvent.GetPosition(elementB)); + } + } + [Fact] public void Mouse_Pointer_Should_Set_Focus_On_Pointer_Pressed() { diff --git a/tests/Avalonia.RenderTests/TestRenderRoot.cs b/tests/Avalonia.RenderTests/TestRenderRoot.cs index 602c5c6e8b..4bbe560fb6 100644 --- a/tests/Avalonia.RenderTests/TestRenderRoot.cs +++ b/tests/Avalonia.RenderTests/TestRenderRoot.cs @@ -67,9 +67,9 @@ namespace Avalonia.Skia.RenderTests { } - public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); + public Point? PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); - public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); + public PixelPoint? PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); public IFocusManager? FocusManager { get; } public IPlatformSettings? PlatformSettings { get; } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 4400d77267..5a34d301df 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -113,9 +113,9 @@ namespace Avalonia.UnitTests { } - public Point PointToClient(PixelPoint p) => p.ToPoint(1); + public Point? PointToClient(PixelPoint p) => p.ToPoint(1); - public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); + public PixelPoint? PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); public void RegisterChildrenNames() From dcb607179b707a6c0e41a39fc697af43c00abc48 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 4 May 2026 12:25:57 +0000 Subject: [PATCH 2/2] fix android file props (#21307) --- .../Platform/Storage/AndroidStorageItem.cs | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 2ee0236828..a5ceb02312 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -546,8 +546,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF { var projection = new[] { - MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded, - MediaStore.IMediaColumns.DateModified + Document.ColumnSize, Document.ColumnLastModified }; using var cursor = Activity.ContentResolver!.Query(Uri, projection, null, null, null); @@ -555,7 +554,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF { try { - var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.Size); + var columnIndex = cursor.GetColumnIndex(Document.ColumnSize); if (columnIndex != -1) { size = (ulong)cursor.GetLong(columnIndex); @@ -569,22 +568,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF try { - var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateAdded); - if (columnIndex != -1) - { - var longValue = cursor.GetLong(columnIndex); - itemDate = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null; - } - } - catch (Exception ex) - { - Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? - .Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex); - } - - try - { - var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateModified); + var columnIndex = cursor.GetColumnIndex(Document.ColumnLastModified); if (columnIndex != -1) { var longValue = cursor.GetLong(columnIndex); @@ -594,7 +578,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF catch (Exception ex) { Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? - .Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex); + .Log(this, "File LastModified metadata reader failed: '{Exception}'", ex); } } }