diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 0288dbce91..6232fa39a1 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -42,7 +42,7 @@ namespace Avalonia.Data.Core.Plugins if (target is IObservable result) { return result; - }; + } // If the observable returns a value type then we need to call Observable.Select on it. // First get the type of T in `IObservable`. diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 889b7e3b82..4f5585ab6c 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -71,7 +71,7 @@ namespace Avalonia.Input.GestureRecognizers { EndGesture(); _tracking = e.Pointer; - _gestureId = ScrollGestureEventArgs.GetNextFreeId();; + _gestureId = ScrollGestureEventArgs.GetNextFreeId(); _trackedRootPoint = e.GetPosition(_target); } } diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 1f3c726e7b..a0e8d4a074 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -65,6 +65,7 @@ namespace Avalonia.Input return default; if (relativeTo == null) return pt; + return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; } diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 2ebf01bcf6..e367f9f74f 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -68,27 +68,29 @@ namespace Avalonia.Input if (dirtyRect.Contains(clientPoint)) { - SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); + var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint); + SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } else if (!_inputRoot.Bounds.Contains(clientPoint)) { - ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } } } private void ClearPointerOver() { - if (_lastPointer is (var pointer, var _)) + if (_lastPointer is (var pointer, var position)) { - ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + var clientPoint = _inputRoot.PointToClient(position); + ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } _lastPointer = null; _lastActivePointerDevice = null; } private void ClearPointerOver(IPointer pointer, IInputRoot root, - ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + ulong timestamp, Point? position, PointerPointProperties properties, KeyModifiers inputModifiers) { var element = root.PointerOverElement; if (element is null) @@ -96,11 +98,10 @@ namespace Avalonia.Input return; } - // Do not pass rootVisual, when we have unknown (negative) position, + // Do not pass rootVisual, when we have unknown position, // so GetPosition won't return invalid values. - var hasPosition = position.X >= 0 && position.Y >= 0; var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer, - hasPosition ? root : null, hasPosition ? position : default, + position.HasValue ? root : null, position.HasValue ? position.Value : default, timestamp, properties, inputModifiers); if (element != null && !element.IsAttachedToVisualTree) diff --git a/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs index 3e516b4f97..e6a95d1ffb 100644 --- a/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs @@ -523,7 +523,7 @@ namespace Avalonia.Layout { firstRealizedElement = _elementManager.GetAt(0); firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); - firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);; + firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); int last = _elementManager.GetRealizedElementCount() - 1; lastRealizedElement = _elementManager.GetAt(last); diff --git a/src/Avalonia.Base/Media/TextTrimming.cs b/src/Avalonia.Base/Media/TextTrimming.cs index b6f5be496f..e2737210be 100644 --- a/src/Avalonia.Base/Media/TextTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrimming.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// public abstract class TextTrimming { - public static char s_defaultEllipsisChar = '\u2026'; + internal const char DefaultEllipsisChar = '\u2026'; /// /// Text is not trimmed. @@ -18,22 +18,22 @@ namespace Avalonia.Media /// /// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text. /// - public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false); + public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(DefaultEllipsisChar, false); /// /// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text. /// - public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true); + public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(DefaultEllipsisChar, true); /// /// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text. /// - public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8); + public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(DefaultEllipsisChar, 8); /// /// Text is trimmed at a character boundary starting from the beginning. An ellipsis (...) is drawn in place of remaining text. /// - public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 0); + public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(DefaultEllipsisChar, 0); /// /// Creates properties that will be used for collapsing lines of text. diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index 0077dd64ac..e2338b9b26 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media /// public readonly struct UnicodeRange { - public static UnicodeRange Default = Parse("0-10FFFD"); + public readonly static UnicodeRange Default = Parse("0-10FFFD"); private readonly UnicodeRangeSegment _single; private readonly IReadOnlyList? _segments = null; diff --git a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs index 613c931165..2a5b8d39c5 100644 --- a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs @@ -3,7 +3,7 @@ using System; namespace Avalonia.Utilities { /// - /// Defines a listener to a event subscribed vis the . + /// Defines a listener to a event subscribed vis the . /// /// The type of the event arguments. public interface IWeakSubscriber where T : EventArgs diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1f3b10c0cc..03c66aff2b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -378,8 +378,6 @@ namespace Avalonia.Controls /// /// Enables or disables resizing of the window. - /// Note that if is set to False then this property - /// has no effect and should be treated as a recommendation for the user setting HasSystemDecorations. /// public bool CanResize { diff --git a/src/Avalonia.MicroCom/MicroComProxyBase.cs b/src/Avalonia.MicroCom/MicroComProxyBase.cs index 140af3e4ef..fe8a2bf9cf 100644 --- a/src/Avalonia.MicroCom/MicroComProxyBase.cs +++ b/src/Avalonia.MicroCom/MicroComProxyBase.cs @@ -65,7 +65,7 @@ namespace Avalonia.MicroCom protected virtual void Dispose(bool disposing) { - if(_nativePointer == null) + if(_nativePointer == IntPtr.Zero) return; if (_ownsHandle) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 15d42789d4..702ae3f8e5 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { @@ -29,8 +27,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput new Thread(()=>InputThread(ctx)).Start(); } - - private unsafe void InputThread(IntPtr ctx) { var fd = libinput_get_fd(ctx); @@ -143,8 +139,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput } } - - public void Initialize(IScreenInfoProvider screen, Action onInput) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs new file mode 100644 index 0000000000..05dd0195b1 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.LinuxFramebuffer.Input.NullInput; + +public class NullInputBackend : IInputBackend +{ + public void Initialize(IScreenInfoProvider screen, Action onInput) + { + } + + public void SetInputRoot(IInputRoot root) + { + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index bf9452e191..730eb0071d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -42,9 +42,9 @@ namespace Avalonia.LinuxFramebuffer Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); - + var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); - + AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) @@ -59,12 +59,12 @@ namespace Avalonia.LinuxFramebuffer AvaloniaLocator.Current.GetService()); } - - internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend) where T : AppBuilderBase, new() + + internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend, IInputBackend inputBackend) where T : AppBuilderBase, new() { var platform = new LinuxFramebufferPlatform(outputBackend); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); - return new LinuxFramebufferLifetime(platform._fb); + return new LinuxFramebufferLifetime(platform._fb, inputBackend); } } @@ -80,13 +80,13 @@ namespace Avalonia.LinuxFramebuffer { _fb = fb; } - + public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input) { _fb = fb; _inputBackend = input; } - + public Control MainView { get => (Control)_topLevel?.Content; @@ -126,7 +126,7 @@ namespace Avalonia.LinuxFramebuffer { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); } - + public void Shutdown(int exitCode) { ExitCode = exitCode; @@ -140,22 +140,22 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1) + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1, IInputBackend inputBackend = default) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }); - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling) + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend); + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend inputBackend = default) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }); - - public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling}); - public static int StartLinuxDrm(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options)); - - public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend); + + public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1, IInputBackend inputBackend = default) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend); + public static int StartLinuxDrm(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null, IInputBackend inputBackend = default) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend); + + public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend outputBackend, IInputBackend inputBackend = default) where T : AppBuilderBase, new() { - var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); + var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); builder.SetupWithLifetime(lifetime); lifetime.Start(args); builder.Instance.Run(lifetime.Token); diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index cbcc55ada0..5c4af5de2e 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -133,10 +133,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator MethodDeclaration(ParseTypeName("void"), "Initialize") .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - - var changesVarName = "c"; - var changesVar = IdentifierName(changesVarName); - server = server.AddMembers( MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra") .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index a83b176484..1ac50446c0 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -356,20 +356,28 @@ namespace Avalonia.Base.UnitTests.Input var impl = CreateTopLevelImplMock(renderer.Object); var invalidateRect = new Rect(0, 0, 15, 15); + var lastClientPosition = new Point(1, 5); + + var result = new List<(object?, string, Point)>(); + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null))); + } Canvas canvas; - var root = CreateInputRoot(impl.Object, new Panel + var root = (Window)CreateInputRoot(impl.Object, new Panel { Children = { (canvas = new Canvas()) } }); + AddEnteredExitedHandlers(HandleEvent, root, canvas); // Let input know about latest device. SetHit(renderer, canvas); - impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition)); Assert.True(canvas.IsPointerOver); SetHit(renderer, canvas); @@ -380,6 +388,52 @@ namespace Avalonia.Base.UnitTests.Input SetHit(renderer, null); renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); Assert.False(canvas.IsPointerOver); + + Assert.Equal( + new[] + { + ((object?)canvas, nameof(InputElement.PointerEntered), lastClientPosition), + (root, nameof(InputElement.PointerEntered), lastClientPosition), + (canvas, nameof(InputElement.PointerExited), lastClientPosition), + (root, nameof(InputElement.PointerExited), lastClientPosition), + }, + result); + } + + [Fact] + public void PointerOver_Invalidation_Should_Use_Previously_Captured_Element() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var invalidateRect = new Rect(0, 0, 15, 15); + + Canvas canvas1, canvas2; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas1 = new Canvas()), + (canvas2 = new Canvas()) + } + }); + + canvas1.PointerMoved += (s, a) => a.Pointer.Capture(canvas1); + + // Let input know about latest device. + SetHit(renderer, canvas1); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + Assert.True(canvas1.IsPointerOver); + Assert.False(canvas2.IsPointerOver); + + SetHit(renderer, canvas2); + renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); + Assert.False(canvas1.IsPointerOver); + Assert.True(canvas2.IsPointerOver); } // https://github.com/AvaloniaUI/Avalonia/issues/7748 diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs index e25ef0a9b8..0bd23d64a9 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs @@ -8,7 +8,7 @@ using Xunit; namespace Avalonia.Base.UnitTests.Input { - public class PointerTests + public class PointerTests : PointerTestsBase { [Fact] public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 48dbfa5985..960c409058 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -389,7 +389,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (textLine.Width > 300 || currentHeight + textLine.Height > 240) { - textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] { TextTrimming.s_defaultEllipsisChar }), 300, defaultProperties)); + textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] { TextTrimming.DefaultEllipsisChar }), 300, defaultProperties)); } currentHeight += textLine.Height;