From da3d8207b8250492c83dbf56ab3458b77439b3c2 Mon Sep 17 00:00:00 2001 From: snowflysky <45072798+snowflysky@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:13:47 +0800 Subject: [PATCH 01/12] macOS - Correct key mapping for scan code 0x18 (OemPlus instead of OemMinus) (#20009) --- native/Avalonia.Native/src/OSX/KeyTransform.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index ba3d809dd9..a6056cef91 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -33,7 +33,7 @@ const KeyInfo keyInfos[] = { 0x1A, AvnPhysicalKeyDigit7, AvnKeyD7, '7' }, { 0x1C, AvnPhysicalKeyDigit8, AvnKeyD8, '8' }, { 0x19, AvnPhysicalKeyDigit9, AvnKeyD9, '9' }, - { 0x18, AvnPhysicalKeyEqual, AvnKeyOemMinus, '-' }, + { 0x18, AvnPhysicalKeyEqual, AvnKeyOemPlus, '=' }, { 0x0A, AvnPhysicalKeyIntlBackslash, AvnKeyOem102, 0 }, { 0x5E, AvnPhysicalKeyIntlRo, AvnKeyOem102, 0 }, { 0x5D, AvnPhysicalKeyIntlYen, AvnKeyOem5, 0 }, From 052bf2d46d596588e7194ed37b23e1a26811d2f0 Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Sat, 8 Nov 2025 00:19:24 +0900 Subject: [PATCH 02/12] [Win] Handle mouse movement in non-client areas of window. (#19922) * V1: Handle MouseLeave Event to handle caption buttons * Update methods --- .../Interop/UnmanagedMethods.cs | 7 ++ .../WindowImpl.CustomCaptionProc.cs | 69 +++++++++++++++---- src/Windows/Avalonia.Win32/WindowImpl.cs | 1 + 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 50a9c62136..da3414ba6a 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -2298,6 +2298,13 @@ namespace Avalonia.Win32.Interop public int dwHoverTime; } + // TrackMouseEvent flags + public const uint TME_HOVER = 0x00000001; + public const uint TME_LEAVE = 0x00000002; + public const uint TME_NONCLIENT = 0x00000010; + public const uint TME_QUERY = 0x40000000; + public const uint TME_CANCEL = 0x80000000; + [Flags] public enum WindowPlacementFlags : uint { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index fc7fe731d0..664315c853 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; @@ -141,24 +142,64 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMOUSEMOVE when !IsMouseInPointerEnabled: case WindowsMessage.WM_NCLBUTTONDOWN when !IsMouseInPointerEnabled: case WindowsMessage.WM_NCLBUTTONUP when !IsMouseInPointerEnabled: - if (lRet == IntPtr.Zero - && ShouldRedirectNonClientInput(hWnd, wParam, lParam)) + if (lRet == IntPtr.Zero) { - e = new RawPointerEventArgs( - _mouseDevice, - unchecked((uint)GetMessageTime()), - Owner, - (WindowsMessage)msg switch + var shouldRedirect = ShouldRedirectNonClientInput(hWnd, wParam, lParam); + + if (shouldRedirect) + { + // Track non-client mouse to receive WM_NCMOUSELEAVE + if (!_trackingNonClientMouse) { - WindowsMessage.WM_NCMOUSEMOVE => RawPointerEventType.Move, - WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType.LeftButtonDown, - WindowsMessage.WM_NCLBUTTONUP => RawPointerEventType.LeftButtonUp, - _ => throw new ArgumentOutOfRangeException(nameof(msg), msg, null) - }, - PointToClient(PointFromLParam(lParam)), - RawInputModifiers.None); + var tm = new TRACKMOUSEEVENT + { + cbSize = Marshal.SizeOf(), + dwFlags = TME_LEAVE | TME_NONCLIENT, + hwndTrack = _hwnd, + dwHoverTime = 0, + }; + TrackMouseEvent(ref tm); + _trackingNonClientMouse = true; + } + + e = new RawPointerEventArgs( + _mouseDevice, + unchecked((uint)GetMessageTime()), + Owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_NCMOUSEMOVE => RawPointerEventType.Move, + WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType.LeftButtonDown, + WindowsMessage.WM_NCLBUTTONUP => RawPointerEventType.LeftButtonUp, + _ => throw new ArgumentOutOfRangeException(nameof(msg), msg, null) + }, + PointToClient(PointFromLParam(lParam)), + RawInputModifiers.None); + } + else if (_trackingNonClientMouse && (WindowsMessage)msg == WindowsMessage.WM_NCMOUSEMOVE) + { + // Mouse moved in NC area but not over caption buttons - send leave event + _trackingNonClientMouse = false; + e = new RawPointerEventArgs( + _mouseDevice, + unchecked((uint)GetMessageTime()), + Owner, + RawPointerEventType.LeaveWindow, + new Point(-1, -1), + RawInputModifiers.None); + } } break; + case WindowsMessage.WM_NCMOUSELEAVE when !IsMouseInPointerEnabled: + _trackingNonClientMouse = false; + e = new RawPointerEventArgs( + _mouseDevice, + unchecked((uint)GetMessageTime()), + Owner, + RawPointerEventType.LeaveWindow, + new Point(-1, -1), + RawInputModifiers.None); + break; case WindowsMessage.WM_NCPOINTERUPDATE when _wmPointerEnabled: case WindowsMessage.WM_NCPOINTERDOWN when _wmPointerEnabled: case WindowsMessage.WM_NCPOINTERUP when _wmPointerEnabled: diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 323539106a..769a8d6768 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -89,6 +89,7 @@ namespace Avalonia.Win32 private IconImpl? _iconImpl; private readonly Dictionary<(Icons type, uint dpi), Win32Icon> _iconCache = new(); private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk + private bool _trackingNonClientMouse; private bool _topmost; private double _scaling = 1; private uint _dpi = 96; From fee3032a043ad1096b174903edd429a53c0ef664 Mon Sep 17 00:00:00 2001 From: zacfromaustinpowder <165237243+zacfromaustinpowder@users.noreply.github.com> Date: Sat, 8 Nov 2025 02:15:55 +1000 Subject: [PATCH 03/12] Fixed Selector.ValidateNestingSelector not calling overrides when iterating up through parent selectors (#19947) * Fixed Selector.ValidateNestingSelector not calling overrides when iterating up through parent selectors Changed ValidateNestingSelector to call recursively, walking up the hierarchy of parent selectors. This means function overrides are taken into account when walking up the tree. Now if a selector has an OrSelector as its parent, it doesn't throw an exception. * Updated ToString methods to surround OrSelectors with parenthesis where useful --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- .../Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 6 +-- src/Avalonia.Base/Styling/OrSelector.cs | 16 ++++++-- .../Styling/PropertyEqualsSelector.cs | 3 +- src/Avalonia.Base/Styling/Selector.cs | 41 +++++++++++-------- src/Avalonia.Base/Styling/Selectors.cs | 3 +- src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- .../Styling/SelectorTests_Or.cs | 19 +++++++++ 11 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index ac28d2bc46..b118ea5561 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -31,7 +31,7 @@ namespace Avalonia.Styling { if (_selectorString == null) { - _selectorString = _parent.ToString(owner) + " > "; + _selectorString = _parent.ToString(owner, true) + " > "; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 6706eb4441..646f7272a5 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -29,7 +29,7 @@ namespace Avalonia.Styling { if (_selectorString == null) { - _selectorString = _parent.ToString(owner) + ' '; + _selectorString = _parent.ToString(owner, true) + ' '; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index 9a541cbba7..dca8a45ef8 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -39,7 +39,7 @@ namespace Avalonia.Styling { if (_selectorString == null) { - _selectorString = $"{_previous?.ToString(owner)}:not({_argument})"; + _selectorString = $"{_previous?.ToString(owner, true)}:not({_argument})"; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index bf6247aba1..e11b81a088 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -109,9 +109,9 @@ namespace Avalonia.Styling public override string ToString(Style? owner) { var expectedCapacity = NthLastChildSelectorName.Length + 8; - var stringBuilder = StringBuilderCache.Acquire(expectedCapacity); - stringBuilder.Append(_previous?.ToString(owner)); - + var stringBuilder = StringBuilderCache.Acquire(expectedCapacity); + stringBuilder.Append(_previous?.ToString(owner, true)); + stringBuilder.Append(':'); stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName); stringBuilder.Append('('); diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index cc77aa9fcf..a58ced7c65 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -45,11 +45,19 @@ namespace Avalonia.Styling internal override Type? TargetType => _targetType ??= EvaluateTargetType(); /// - public override string ToString(Style? owner) + public override string ToString(Style? owner) => ToString(owner, false); + + /// + internal override string ToString(Style? owner, bool hasNext) { if (_selectorString == null) { - _selectorString = string.Join(", ", _selectors.Select(x => x.ToString(owner))); + _selectorString = string.Join(", ", _selectors.Select(x => x.ToString(owner, true))); + + if (hasNext) + { + _selectorString = $"({_selectorString})"; + } } return _selectorString; @@ -97,13 +105,13 @@ namespace Avalonia.Styling private protected override Selector? MovePrevious() => null; private protected override Selector? MovePreviousOrParent() => null; - internal override void ValidateNestingSelector(bool inControlTheme) + internal override void ValidateNestingSelector(bool inControlTheme, int templateCount = 0) { var count = _selectors.Count; for (var i = 0; i < count; i++) { - _selectors[i].ValidateNestingSelector(inControlTheme); + _selectors[i].ValidateNestingSelector(inControlTheme, templateCount); } } diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 1d684eeca3..316b7a2853 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -45,7 +45,7 @@ namespace Avalonia.Styling if (_previous != null) { - builder.Append(_previous.ToString(owner)); + builder.Append(_previous.ToString(owner, true)); } builder.Append('['); @@ -85,7 +85,6 @@ namespace Avalonia.Styling ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } - } private protected override Selector? MovePrevious() => _previous; diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index cb3ddc343c..9102d2e770 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -47,7 +47,7 @@ namespace Avalonia.Styling // right-to-left, so MatchUntilCombinator reverses this order because the type selector // will be on the left. var match = MatchUntilCombinator(control, this, parent, subscribe, out var combinator); - + // If the pre-combinator selector matches, we can now match the combinator, if any. if (match.IsMatch && combinator is object) { @@ -76,6 +76,10 @@ namespace Avalonia.Styling /// The owner style. public abstract string ToString(Style? owner); + /// + /// Whether there is a selector that comes after this one. + internal virtual string ToString(Style? owner, bool hasNext) => ToString(owner); + /// /// Evaluates the selector for a match. /// @@ -100,30 +104,31 @@ namespace Avalonia.Styling /// private protected abstract Selector? MovePreviousOrParent(); - internal virtual void ValidateNestingSelector(bool inControlTheme) + internal virtual void ValidateNestingSelector(bool inControlTheme, int templateCount = 0) { var s = this; - var templateCount = 0; - do + if (inControlTheme) { - if (inControlTheme) - { - if (!s.InTemplate && s.IsCombinator) - throw new InvalidOperationException( - "ControlTheme style may not directly contain a child or descendent selector."); - if (s is TemplateSelector && templateCount++ > 0) - throw new InvalidOperationException( - "ControlTemplate styles cannot contain multiple template selectors."); - } + if (!s.InTemplate && s.IsCombinator) + throw new InvalidOperationException( + "ControlTheme style may not directly contain a child or descendent selector."); + if (s is TemplateSelector && templateCount++ > 0) + throw new InvalidOperationException( + "ControlTemplate styles cannot contain multiple template selectors."); + } - var previous = s.MovePreviousOrParent(); + var previous = s.MovePreviousOrParent(); - if (previous is null && s is not NestingSelector) + if (previous is null) + { + if (s is not NestingSelector) throw new InvalidOperationException("Child styles must have a nesting selector."); - - s = previous; - } while (s is not null); + } + else + { + previous.ValidateNestingSelector(inControlTheme, templateCount); + } } private static SelectorMatch MatchUntilCombinator( diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs index d7406f2164..a58ed3f11d 100644 --- a/src/Avalonia.Base/Styling/Selectors.cs +++ b/src/Avalonia.Base/Styling/Selectors.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace Avalonia.Styling { @@ -124,7 +123,7 @@ namespace Avalonia.Styling { return new NotSelector(previous, argument(null)); } - + /// /// Returns a selector which inverts the results of selector argument. /// diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index 1fa2ca2d0f..4fcccef87e 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -30,7 +30,7 @@ namespace Avalonia.Styling { if (_selectorString == null) { - _selectorString = _parent.ToString(owner) + " /template/ "; + _selectorString = _parent.ToString(owner, true) + " /template/ "; } return _selectorString; diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 81b204761b..4fba8c02c6 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -143,7 +143,7 @@ namespace Avalonia.Styling if (_previous != null) { - builder.Append(_previous.ToString(owner)); + builder.Append(_previous.ToString(owner, true)); } if (TargetType != null) diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Or.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Or.cs index fb5d54bd1f..c013778128 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Or.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Or.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls; using Avalonia.Styling; using Xunit; @@ -89,6 +90,24 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(null, target.TargetType); } + + [Fact] + public void ValidateNestingSelector_Checks_Children_When_Parent_Is_An_OrSelector() + { + var target = Selectors.Or( + default(Selector).Class("foo"), + default(Selector).Class("bar") + ).Name("baz"); + + Assert.Throws(() => target.ValidateNestingSelector(false)); + + target = Selectors.Or( + default(Selector).Nesting().Class("foo"), + default(Selector).Nesting().Class("bar") + ).Name("baz"); + + target.ValidateNestingSelector(false); + } public class Control1 : Control { From 83b10db596d8781c29d512b362db61a2560d95d8 Mon Sep 17 00:00:00 2001 From: aguahombre Date: Fri, 7 Nov 2025 16:19:57 +0000 Subject: [PATCH 04/12] Fix ServerCompositionSimplePen memory leak (#19958) * Fix for Issue#16451 * Remove the pen from the brush observers without queuing the pen for invalidation. --- .../Drawing/ServerCompositionSimplePen.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs index 8e99a1cc01..725959c728 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs @@ -1,12 +1,19 @@ -using System; using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionSimplePen : IPen { IDashStyle? IPen.DashStyle => DashStyle; -} \ No newline at end of file + + /// + public override void Dispose() + { + // Remove the pen from the brush observers. + // Without this, the pen was being retained in memory by long lived brush resources (e.g. those defined in + // the theme or app resources), hence was causing memory leaks; see Issue #16451 + RemoveObserversFromProperty(ref _brush); + _brush = null; + base.Dispose(); + } +} From 7aed580721af8793f2a1ace83e7a9c254c614abb Mon Sep 17 00:00:00 2001 From: Andrey Rusyaev Date: Fri, 7 Nov 2025 21:47:02 +0400 Subject: [PATCH 05/12] Add Record struct and field to ComVariant to ensure that it has proper binary size (4 pointers (16 bytes) on a 32-bit processor, 3 pointers (24 bytes) on a 64-bit processor). (#20017) Based on dotnet/runtime implementation https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/ComVariant.cs --- .../Marshalling/ComVariant.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs index d5da462f8c..a7fcd6776c 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs @@ -18,6 +18,25 @@ internal struct ComVariant : IDisposable internal const short VARIANT_TRUE = -1; internal const short VARIANT_FALSE = 0; +#if DEBUG + static unsafe ComVariant() + { + // Variant size is the size of 4 pointers (16 bytes) on a 32-bit processor, + // and 3 pointers (24 bytes) on a 64-bit processor. + // See definition in oaidl.h in the Windows SDK. + int variantSize = sizeof(ComVariant); + if (IntPtr.Size == 4) + { + Debug.Assert(variantSize == (4 * IntPtr.Size)); + } + else + { + Debug.Assert(IntPtr.Size == 8); + Debug.Assert(variantSize == (3 * IntPtr.Size)); + } + } +#endif + // Most of the data types in the Variant are carried in _typeUnion [FieldOffset(0)] private TypeUnion _typeUnion; @@ -32,6 +51,13 @@ internal struct ComVariant : IDisposable public UnionTypes _unionTypes; } + [StructLayout(LayoutKind.Sequential)] + private struct Record + { + public IntPtr _record; + public IntPtr _recordInfo; + } + [StructLayout(LayoutKind.Explicit)] private unsafe struct UnionTypes { @@ -56,6 +82,7 @@ internal struct ComVariant : IDisposable [FieldOffset(0)] public IntPtr _dispatch; [FieldOffset(0)] public IntPtr _pvarVal; [FieldOffset(0)] public IntPtr _byref; + [FieldOffset(0)] public Record _record; [FieldOffset(0)] public SafeArrayRef parray; [FieldOffset(0)] public SafeArrayRef*pparray; } From 7bbf4e1d710ea5440a3bd378adceb2e8da512a41 Mon Sep 17 00:00:00 2001 From: Sattar Imamov Date: Sat, 8 Nov 2025 12:13:37 +0100 Subject: [PATCH 06/12] - Fixed XButtons event handler for MacOs (#19997) --- native/Avalonia.Native/src/OSX/AvnView.mm | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 2ff83547a0..d4942afced 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -456,13 +456,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) switch(event.buttonNumber) { case 2: - case 3: [self mouseEvent:event withType:MiddleButtonDown]; break; - case 4: + case 3: [self mouseEvent:event withType:XButton1Down]; break; - case 5: + case 4: [self mouseEvent:event withType:XButton2Down]; break; @@ -487,13 +486,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) switch(event.buttonNumber) { case 2: - case 3: [self mouseEvent:event withType:MiddleButtonUp]; break; - case 4: + case 3: [self mouseEvent:event withType:XButton1Up]; break; - case 5: + case 4: [self mouseEvent:event withType:XButton2Up]; break; From a12b8d706196be00e3f38062bb55575d2eca05bc Mon Sep 17 00:00:00 2001 From: Bobby Cannon Date: Sat, 8 Nov 2025 06:16:42 -0500 Subject: [PATCH 07/12] Fix for previous PR19985 - AccessibilityNodeInfoCompat.Checked (#19991) * fixed * fixed * fixed naming * just not having a good PR --- .../Automation/ToggleNodeInfoProvider.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs b/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs index 907bd2e3a0..cbd64e3b18 100644 --- a/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs +++ b/src/Android/Avalonia.Android/Automation/ToggleNodeInfoProvider.cs @@ -9,13 +9,18 @@ namespace Avalonia.Android.Automation { internal class ToggleNodeInfoProvider : NodeInfoProvider { - private PropertyInfo? _checkedProperty; + private static PropertyInfo? s_checkedProperty; public ToggleNodeInfoProvider(ExploreByTouchHelper owner, AutomationPeer peer, int virtualViewId) : base(owner, peer, virtualViewId) { } + static ToggleNodeInfoProvider() + { + s_checkedProperty = typeof(AccessibilityNodeInfoCompat).GetProperty(nameof(AccessibilityNodeInfoCompat.Checked)); + } + public override bool PerformNodeAction(int action, Bundle? arguments) { IToggleProvider provider = GetProvider(); @@ -36,11 +41,11 @@ namespace Avalonia.Android.Automation IToggleProvider provider = GetProvider(); - _checkedProperty ??= nodeInfo.GetType().GetProperty(nameof(nodeInfo.Checked)); - if (_checkedProperty?.PropertyType == typeof(int)) + s_checkedProperty ??= nodeInfo.GetType().GetProperty(nameof(nodeInfo.Checked)); + if (s_checkedProperty?.PropertyType == typeof(int)) { // Needed for Xamarin.AndroidX.Core 1.17+ - _checkedProperty.SetValue(this, + s_checkedProperty.SetValue(this, provider.ToggleState switch { ToggleState.On => 1, @@ -48,10 +53,10 @@ namespace Avalonia.Android.Automation _ => 0 }); } - else if (_checkedProperty?.PropertyType == typeof(bool)) + else if (s_checkedProperty?.PropertyType == typeof(bool)) { // Needed for Xamarin.AndroidX.Core < 1.17 - _checkedProperty.SetValue(this, provider.ToggleState == ToggleState.On); + s_checkedProperty.SetValue(this, provider.ToggleState == ToggleState.On); } nodeInfo.Checkable = true; From 1936725f2d8a3184c538f5767b372aa381850109 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 8 Nov 2025 14:35:20 +0100 Subject: [PATCH 08/12] Cache platform font manager TryMatchCharacter result (#19987) --- src/Avalonia.Base/Media/FontManager.cs | 7 +- .../Media/Fonts/SystemFontCollection.cs | 62 +++++++++++----- .../Platform/IFontManagerImpl.cs | 15 ++++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 71 ++++++++++++++----- .../Media/FontManagerTests.cs | 21 ++++++ 5 files changed, 141 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index c8d8042e83..5a49511a5a 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -287,6 +287,8 @@ namespace Avalonia.Media } if (TryGetFontCollection(source, out var fontCollection) && + // With composite fonts we need to first check if the font collection contains the family if not we skip it + fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) && fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) { return true; @@ -306,8 +308,9 @@ namespace Avalonia.Media } } - //Try to find a match with the system font manager - return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); + //Try to find a match with the system font collection + return SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily?.Name, + culture, out typeface); } internal IReadOnlyList GetFamilyTypefaces(FontFamily fontFamily) diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 3a98a30b90..3b0c71ce20 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using Avalonia.Platform; @@ -15,7 +16,7 @@ namespace Avalonia.Media.Fonts public SystemFontCollection(FontManager fontManager) { _fontManager = fontManager; - _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x=> !string.IsNullOrEmpty(x)).ToList(); + _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x)).ToList(); } public override Uri Key => FontManager.SystemFontsKey; @@ -144,21 +145,6 @@ namespace Avalonia.Media.Fonts } return; - - void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) - { - var typefaces = _glyphTypefaceCache.GetOrAdd(familyName, - x => - { - _familyNames.Insert(0, familyName); - - return new ConcurrentDictionary(); - }); - - typefaces.TryAdd( - new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), - glyphTypeface); - } } public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) @@ -172,5 +158,49 @@ namespace Avalonia.Media.Fonts return false; } + + public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName, + CultureInfo? culture, out Typeface match) + { + //TODO12: Think about removing familyName parameter + match = default; + + if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2) + { + if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var glyphTypeface)) + { + AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); + + match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight, + glyphTypeface.Stretch); + + return true; + } + + return false; + } + else + { + return _fontManager.PlatformImpl.TryMatchCharacter(codepoint, style, weight, stretch, culture, out match); + } + } + + private void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) + { + // Add family name to the collection if not exists + if (!_familyNames.Contains(familyName)) + { + _familyNames.Add(familyName); + } + + // Get or create the typefaces dictionary for the family name + if (!_glyphTypefaceCache.TryGetValue(familyName, out var typefaces)) + { + _glyphTypefaceCache[familyName] = typefaces = new ConcurrentDictionary(); + } + + // Add the glyph typeface to the cache + typefaces.TryAdd(new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), glyphTypeface); + } } } diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index ce9f85a5e2..42c9b3623f 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -65,6 +65,21 @@ namespace Avalonia.Platform internal interface IFontManagerImpl2 : IFontManagerImpl { + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// + /// The codepoint to match against. + /// The font style. + /// The font weight. + /// The font stretch. + /// The culture. + /// The matching typeface. + /// + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface); + /// /// Tries to get a list of typefaces for the specified family name. /// diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index eb1833193c..e013124cf1 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,8 +1,11 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Text.RegularExpressions; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -15,6 +18,7 @@ namespace Avalonia.Skia public string GetDefaultFontFamilyName() { + return SKTypeface.Default.FamilyName; } @@ -32,6 +36,53 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey) + { + if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface)) + { + fontKey = default; + + return false; + } + + fontKey = new Typeface( + skTypeface.FamilyName, + skTypeface.FontStyle.Slant.ToAvalonia(), + (FontWeight)skTypeface.FontStyle.Weight, + (FontStretch)skTypeface.FontStyle.Width); + + skTypeface.Dispose(); + + return true; + + } + + public bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface)) + { + glyphTypeface = null; + + return false; + } + + glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + + return true; + } + + private bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, + [NotNullWhen(true)] out SKTypeface? skTypeface) { SKFontStyle skFontStyle; @@ -59,23 +110,9 @@ namespace Avalonia.Skia t_languageTagBuffer ??= new string[1]; t_languageTagBuffer[0] = culture.Name; - using var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); + skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); - if (skTypeface != null) - { - // ToDo: create glyph typeface here to get the correct style/weight/stretch - fontKey = new Typeface( - skTypeface.FamilyName, - skTypeface.FontStyle.Slant.ToAvalonia(), - (FontWeight)skTypeface.FontStyle.Weight, - (FontStretch)skTypeface.FontStyle.Width); - - return true; - } - - fontKey = default; - - return false; + return skTypeface != null; } public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 2713e7133b..788815ec41 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -110,6 +110,27 @@ namespace Avalonia.Skia.UnitTests.Media } } + [Fact] + public void Should_Cache_MatchCharacter() + { + var fontManagerImpl = new CustomFontManagerImpl(); + + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl))) + { + var emoji = Codepoint.ReadAt("😀", 0, out _); + + Assert.True(FontManager.Current.TryMatchCharacter((int)emoji, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var firstMatch)); + + var firstGlyphTypeface = firstMatch.GlyphTypeface; + + Assert.True(FontManager.Current.TryMatchCharacter((int)emoji, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var secondMatch)); + + var secondGlyphTypeface = secondMatch.GlyphTypeface; + + Assert.Equal(firstGlyphTypeface, secondGlyphTypeface); + } + } + [Fact] public void Should_Load_Embedded_DefaultFontFamily() { From d500f9112101e440f334904a232e71a0f12bafc1 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:42:49 +0100 Subject: [PATCH 09/12] Enable use of new VS extension with ControlCatalog by compiling XAML files as AvaloniaXaml (#19971) Removed manual InitializeComponent definitions Replaced uses of Get, FindControl, etc. with generated fields --- samples/ControlCatalog/ControlCatalog.csproj | 4 +- .../ControlCatalog/DecoratedWindow.xaml.cs | 58 ++++----- samples/ControlCatalog/MainView.xaml | 27 +++-- samples/ControlCatalog/MainView.xaml.cs | 113 ++++++++---------- samples/ControlCatalog/MainWindow.xaml | 5 +- samples/ControlCatalog/MainWindow.xaml.cs | 13 +- .../Pages/AcceleratorPage.xaml.cs | 10 +- .../ControlCatalog/Pages/AcrylicPage.xaml.cs | 13 +- .../Pages/AdornerLayerPage.xaml.cs | 30 ++--- .../Pages/AutoCompleteBoxPage.xaml.cs | 35 ++---- .../ControlCatalog/Pages/BorderPage.xaml.cs | 10 +- .../Pages/ButtonSpinnerPage.xaml.cs | 12 +- samples/ControlCatalog/Pages/ButtonsPage.xaml | 2 +- .../ControlCatalog/Pages/ButtonsPage.xaml.cs | 16 +-- .../Pages/CalendarDatePickerPage.xaml.cs | 29 ++--- .../ControlCatalog/Pages/CalendarPage.xaml.cs | 24 ++-- .../ControlCatalog/Pages/CanvasPage.xaml.cs | 10 +- .../ControlCatalog/Pages/CarouselPage.xaml.cs | 40 ++----- .../ControlCatalog/Pages/CheckBoxPage.xaml.cs | 10 +- .../Pages/ClipboardPage.xaml.cs | 14 +-- .../Pages/ColorPickerPage.xaml.cs | 10 +- .../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 14 +-- .../Pages/CompositionPage.axaml.cs | 8 +- .../Pages/ContainerQueryPage.xaml.cs | 10 +- .../Pages/ContextFlyoutPage.xaml | 15 +-- .../Pages/ContextFlyoutPage.xaml.cs | 48 ++------ .../ControlCatalog/Pages/ContextMenuPage.xaml | 4 +- .../Pages/ContextMenuPage.xaml.cs | 32 +---- .../ControlCatalog/Pages/CursorPage.xaml.cs | 10 +- .../Pages/CustomDrawing.xaml.cs | 43 ++----- .../ControlCatalog/Pages/DataGridPage.xaml.cs | 7 +- .../Pages/DateTimePickerPage.xaml.cs | 17 +-- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 77 ++++++------ .../Pages/DragAndDropPage.xaml.cs | 19 +-- .../ControlCatalog/Pages/ExpanderPage.xaml.cs | 13 +- .../ControlCatalog/Pages/FlyoutsPage.axaml.cs | 14 +-- samples/ControlCatalog/Pages/GesturePage.cs | 53 +++----- .../ControlCatalog/Pages/ImagePage.xaml.cs | 33 ++--- .../Pages/LayoutTransformControlPage.xaml.cs | 10 +- .../ControlCatalog/Pages/ListBoxPage.xaml.cs | 8 +- samples/ControlCatalog/Pages/MenuPage.xaml.cs | 17 +-- .../Pages/NativeEmbedPage.xaml.cs | 23 ++-- .../Pages/NotificationsPage.xaml.cs | 12 +- .../Pages/NumericUpDownPage.xaml.cs | 9 +- .../Pages/OpenGl/OpenGlLeasePage.xaml | 6 +- .../Pages/OpenGl/OpenGlLeasePage.xaml.cs | 21 +--- .../ControlCatalog/Pages/OpenGlPage.xaml.cs | 17 ++- .../Pages/PlatformInfoPage.xaml.cs | 15 +-- .../ControlCatalog/Pages/PointersPage.xaml | 4 + .../ControlCatalog/Pages/PointersPage.xaml.cs | 27 +---- .../Pages/ProgressBarPage.xaml.cs | 10 +- .../Pages/RadioButtonPage.xaml.cs | 10 +- .../Pages/ScrollViewerPage.xaml.cs | 8 +- .../ControlCatalog/Pages/SliderPage.xaml.cs | 10 +- .../Pages/SplitViewPage.xaml.cs | 13 +- .../Pages/TabControlPage.xaml.cs | 12 +- .../ControlCatalog/Pages/TabStripPage.xaml.cs | 10 +- .../Pages/TextBlockPage.xaml.cs | 10 +- .../ControlCatalog/Pages/TextBoxPage.xaml.cs | 10 +- .../Pages/ToggleSwitchPage.xaml.cs | 13 +- .../ControlCatalog/Pages/ToolTipPage.xaml.cs | 12 +- .../TransitioningContentControlPage.axaml.cs | 7 -- .../ControlCatalog/Pages/TreeViewPage.xaml.cs | 8 +- .../ControlCatalog/Pages/ViewboxPage.xaml.cs | 8 +- .../Pages/WindowCustomizationsPage.xaml.cs | 13 +- .../Views/CustomNotificationView.xaml.cs | 10 +- 66 files changed, 362 insertions(+), 873 deletions(-) diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index cff0bb4e92..7bbff55907 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -9,9 +9,9 @@ %(Filename) - + Designer - + diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index eccfaca60f..bb87d982ba 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -1,53 +1,45 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using System; using Avalonia.Input; namespace ControlCatalog { - public class DecoratedWindow : Window + public partial class DecoratedWindow : Window { public DecoratedWindow() { - this.InitializeComponent(); - } - - void SetupSide(string name, StandardCursorType cursor, WindowEdge edge) - { - var ctl = this.Get(name); - ctl.Cursor = new Cursor(cursor); - ctl.PointerPressed += (i, e) => - { - if (WindowState == WindowState.Normal) - BeginResizeDrag(edge, e); - }; - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - this.Get("TitleBar").PointerPressed += (i, e) => + InitializeComponent(); + TitleBar.PointerPressed += (i, e) => { BeginMoveDrag(e); }; - SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); - SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); - SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North); - SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South); - SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest); - SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast); - SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest); - SetupSide("BottomRight", StandardCursorType.BottomRightCorner, WindowEdge.SouthEast); - this.Get - - - "; - var mfxt = this.Get("MenuFlyoutXamlText"); + var mfxt = this.MenuFlyoutXamlText; mfxt.Text = ""; - var afxt = this.Get("AttachedFlyoutXamlText"); + var afxt = this.AttachedFlyoutXamlText; afxt.Text = "\n" + " \n" + " \n" + @@ -63,7 +59,7 @@ namespace ControlCatalog.Pages "\n\n In DoubleTapped handler:\n" + "FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);"; - var sfxt = this.Get("SharedFlyoutXamlText"); + var sfxt = this.SharedFlyoutXamlText; sfxt.Text = "Declare a flyout in Resources:\n" + "\n" + " \n" + diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs index 9164384eae..c480b512b4 100644 --- a/samples/ControlCatalog/Pages/GesturePage.cs +++ b/samples/ControlCatalog/Pages/GesturePage.cs @@ -1,75 +1,61 @@ using System; -using System.Numerics; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.Markup.Xaml; using Avalonia.Rendering.Composition; using Avalonia.Utilities; namespace ControlCatalog.Pages { - public class GesturePage : UserControl + public partial class GesturePage : UserControl { private bool _isInit; private double _currentScale; public GesturePage() { - this.InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); + InitializeComponent(); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - if(_isInit) + if (_isInit) { return; } _isInit = true; - SetPullHandlers(this.Find("TopPullZone"), false); - SetPullHandlers(this.Find("BottomPullZone"), true); - SetPullHandlers(this.Find("RightPullZone"), true); - SetPullHandlers(this.Find("LeftPullZone"), false); + SetPullHandlers(TopPullZone, false); + SetPullHandlers(BottomPullZone, true); + SetPullHandlers(RightPullZone, true); + SetPullHandlers(LeftPullZone, false); - var image = this.Get("PinchImage"); + var image = PinchImage; SetPinchHandlers(image); - var reset = this.Get