diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index c38124e6da..f4c3aa7596 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -8,6 +8,7 @@ using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Animation.Easings; using Avalonia.Controls.Platform; using Avalonia.Media; +using Avalonia.Threading; using AndroidWindow = Android.Views.Window; namespace Avalonia.Android.Platform @@ -40,7 +41,7 @@ namespace Avalonia.Android.Platform if (oldState != value && Build.VERSION.SdkInt <= BuildVersionCodes.Q) { var currentRect = OccludedRect; - StateChanged?.Invoke(this, new InputPaneStateEventArgs(value, _previousRect, currentRect, TimeSpan.Zero, null)); + NotifyStateChanged(value, _previousRect, currentRect, TimeSpan.Zero, null); _previousRect = currentRect; } } @@ -151,7 +152,12 @@ namespace Avalonia.Android.Platform private void NotifySafeAreaChanged(Thickness safeAreaPadding) { - SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)); + Dispatcher.UIThread.Send(_ => SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding))); + } + + private void NotifyStateChanged(InputPaneState newState, Rect? startRect, Rect endRect, TimeSpan animationDuration, IEasing easing) + { + Dispatcher.UIThread.Send(_ => StateChanged?.Invoke(this, new InputPaneStateEventArgs(newState, startRect, endRect, animationDuration, easing))); } public void OnGlobalLayout() @@ -294,7 +300,7 @@ namespace Avalonia.Android.Platform var duration = TimeSpan.FromMilliseconds(animation.DurationMillis); bool isOpening = State == InputPaneState.Open; - StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, isOpening ? upperRect : lowerRect, isOpening ? lowerRect : upperRect, duration, new AnimationEasing(animation.Interpolator))); + NotifyStateChanged(State, isOpening ? upperRect : lowerRect, isOpening ? lowerRect : upperRect, duration, new AnimationEasing(animation.Interpolator)); } } diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index a63682a0b9..e6cc6b872d 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -3,6 +3,7 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Platform @@ -50,7 +51,8 @@ namespace Avalonia.Platform protected void OnColorValuesChanged(PlatformColorValues colorValues) { - ColorValuesChanged?.Invoke(this, colorValues); + Dispatcher.UIThread.Send( + _ => ColorValuesChanged?.Invoke(this, colorValues)); } } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index dbf95fb609..5a2dca4cf2 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -635,7 +635,7 @@ public partial class Dispatcher /// /// When on the same thread with Send priority, callback is executed immediately, without changing synchronization context. /// - internal void Send(SendOrPostCallback action, object? arg, DispatcherPriority? priority = null) + internal void Send(SendOrPostCallback action, object? arg = null, DispatcherPriority? priority = null) { _ = action ?? throw new ArgumentNullException(nameof(action)); priority ??= DispatcherPriority.Send; diff --git a/src/Avalonia.Controls/Platform/IInputPane.cs b/src/Avalonia.Controls/Platform/IInputPane.cs index c36788570c..a272879dd2 100644 --- a/src/Avalonia.Controls/Platform/IInputPane.cs +++ b/src/Avalonia.Controls/Platform/IInputPane.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Animation.Easings; using Avalonia.Metadata; +using Avalonia.Threading; namespace Avalonia.Controls.Platform { @@ -26,6 +27,20 @@ namespace Avalonia.Controls.Platform event EventHandler? StateChanged; } + [PrivateApi] + public abstract class InputPaneBase : IInputPane + { + public virtual InputPaneState State { get; protected set; } + public virtual Rect OccludedRect { get; protected set; } + public event EventHandler? StateChanged; + + protected void OnStateChanged(InputPaneStateEventArgs eventArgs) + { + Dispatcher.UIThread.Send( + _ => StateChanged?.Invoke(this, eventArgs)); + } + } + /// /// The input pane opened state. /// diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index c604b89e5c..a8b692c82a 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -1,11 +1,11 @@ using System; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Threading; #nullable enable namespace Avalonia.Controls.Platform { - [Unstable] [NotClientImplementable] public interface IInsetsManager { @@ -34,6 +34,23 @@ namespace Avalonia.Controls.Platform /// event EventHandler? SafeAreaChanged; } + + [PrivateApi] + public abstract class InsetsManagerBase : IInsetsManager + { + public virtual bool? IsSystemBarVisible { get; set; } + public virtual bool DisplayEdgeToEdge { get; set; } + public virtual Thickness SafeAreaPadding { get; protected set; } + public virtual Color? SystemBarColor { get; set; } + public event EventHandler? SafeAreaChanged; + + protected void OnSafeAreaChanged(SafeAreaChangedArgs eventArgs) + { + Dispatcher.UIThread.Send( + _ => SafeAreaChanged?.Invoke(this, eventArgs), + null, DispatcherPriority.Send); + } + } public class SafeAreaChangedArgs : EventArgs { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 23841a8130..70e531a789 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -272,7 +272,7 @@ namespace Avalonia.Controls systemNavigationManager.BackRequested += (_, e) => { e.RoutedEvent = BackRequestedEvent; - RaiseEvent(e); + Dispatcher.UIThread.Send(_ => RaiseEvent(e)); }; } @@ -306,7 +306,7 @@ namespace Avalonia.Controls if (backRequested) { var backRequestedEventArgs = new RoutedEventArgs(BackRequestedEvent); - RaiseEvent(backRequestedEventArgs); + Dispatcher.UIThread.Send(_ => RaiseEvent(backRequestedEventArgs)); e.Handled = backRequestedEventArgs.Handled; } @@ -739,7 +739,7 @@ namespace Avalonia.Controls private void HandleScalingChanged(double scaling) { LayoutHelper.InvalidateSelfAndChildrenMeasure(this); - ScalingChanged?.Invoke(this, EventArgs.Empty); + Dispatcher.UIThread.Send(_ => ScalingChanged?.Invoke(this, EventArgs.Empty)); } private void HandleTransparencyLevelChanged(WindowTransparencyLevel transparencyLevel) @@ -786,14 +786,17 @@ namespace Avalonia.Controls protected virtual void OnOpened(EventArgs e) { FrameSize = PlatformImpl?.FrameSize; - Opened?.Invoke(this, e); - } + Dispatcher.UIThread.Send(_ => Opened?.Invoke(this, e)); + } /// /// Raises the event. /// /// The event args. - protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); + protected virtual void OnClosed(EventArgs e) + { + Dispatcher.UIThread.Send(_ => Closed?.Invoke(this, e)); + } /// /// Tries to get a service from an , logging a diff --git a/src/Browser/Avalonia.Browser/BrowserInputPane.cs b/src/Browser/Avalonia.Browser/BrowserInputPane.cs index 0b96599fcc..be132ee956 100644 --- a/src/Browser/Avalonia.Browser/BrowserInputPane.cs +++ b/src/Browser/Avalonia.Browser/BrowserInputPane.cs @@ -5,17 +5,13 @@ using Avalonia.Controls.Platform; namespace Avalonia.Browser; -internal class BrowserInputPane : IInputPane +internal class BrowserInputPane : InputPaneBase { public BrowserInputPane(JSObject container) { InputHelper.SubscribeKeyboardGeometryChange(container, OnGeometryChange); } - public InputPaneState State { get; private set; } - public Rect OccludedRect { get; private set; } - public event EventHandler? StateChanged; - private bool OnGeometryChange(JSObject args) { var oldState = (OccludedRect, State); @@ -29,7 +25,7 @@ internal class BrowserInputPane : IInputPane if (oldState != (OccludedRect, State)) { - StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, null, OccludedRect)); + OnStateChanged(new InputPaneStateEventArgs(State, null, OccludedRect)); } return true; diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs index 0f64003699..dac5a3f14a 100644 --- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -10,9 +10,9 @@ using static Avalonia.Controls.Platform.IInsetsManager; namespace Avalonia.Browser { - internal class BrowserInsetsManager : IInsetsManager + internal class BrowserInsetsManager : InsetsManagerBase { - public bool? IsSystemBarVisible + public override bool? IsSystemBarVisible { get { @@ -24,11 +24,9 @@ namespace Avalonia.Browser } } - public bool DisplayEdgeToEdge { get; set; } + public override bool DisplayEdgeToEdge { get; set; } - public event EventHandler? SafeAreaChanged; - - public Thickness SafeAreaPadding + public override Thickness SafeAreaPadding { get { @@ -38,11 +36,11 @@ namespace Avalonia.Browser } } - public Color? SystemBarColor { get; set; } + public override Color? SystemBarColor { get; set; } public void NotifySafeAreaPaddingChanged() { - SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + OnSafeAreaChanged(new SafeAreaChangedArgs(SafeAreaPadding)); } } } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs b/src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs index cc20d06dc7..73195ad500 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs @@ -8,7 +8,7 @@ using MicroCom.Runtime; namespace Avalonia.Win32.Input; -internal unsafe class WindowsInputPane : IInputPane, IDisposable +internal unsafe class WindowsInputPane : InputPaneBase, IDisposable { private static readonly Lazy s_inputPaneSupported = new(() => WinRTApiInformation.IsTypePresent("Windows.UI.ViewManagement.InputPane")); @@ -48,12 +48,6 @@ internal unsafe class WindowsInputPane : IInputPane, IDisposable return null; } - - public InputPaneState State { get; private set; } - - public Rect OccludedRect { get; private set; } - - public event EventHandler? StateChanged; private void OnStateChanged(bool showing, UnmanagedMethods.RECT? prcInputPaneScreenLocation) { @@ -65,7 +59,7 @@ internal unsafe class WindowsInputPane : IInputPane, IDisposable if (oldState != (OccludedRect, State)) { - StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, null, OccludedRect)); + OnStateChanged(new InputPaneStateEventArgs(State, null, OccludedRect)); } } diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs index d1439fcd3f..105e208ee9 100644 --- a/src/iOS/Avalonia.iOS/InsetsManager.cs +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -4,7 +4,7 @@ using Avalonia.Media; namespace Avalonia.iOS; -internal class InsetsManager : IInsetsManager +internal class InsetsManager : InsetsManagerBase { private IAvaloniaViewController? _controller; private bool _displayEdgeToEdge = true; @@ -16,13 +16,13 @@ internal class InsetsManager : IInsetsManager { _controller.SafeAreaPaddingChanged += (_, _) => { - SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + OnSafeAreaChanged(new SafeAreaChangedArgs(SafeAreaPadding)); DisplayEdgeToEdgeChanged?.Invoke(this, _displayEdgeToEdge); }; } } - public bool? IsSystemBarVisible + public override bool? IsSystemBarVisible { get => _controller?.PrefersStatusBarHidden == false; set @@ -33,10 +33,9 @@ internal class InsetsManager : IInsetsManager } } } - public event EventHandler? SafeAreaChanged; public event EventHandler? DisplayEdgeToEdgeChanged; - public bool DisplayEdgeToEdge + public override bool DisplayEdgeToEdge { get => _displayEdgeToEdge; set @@ -45,12 +44,12 @@ internal class InsetsManager : IInsetsManager { _displayEdgeToEdge = value; DisplayEdgeToEdgeChanged?.Invoke(this, value); - SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + OnSafeAreaChanged(new SafeAreaChangedArgs(SafeAreaPadding)); } } } - public Thickness SafeAreaPadding => _displayEdgeToEdge ? _controller?.SafeAreaPadding ?? default : default; + public override Thickness SafeAreaPadding => _displayEdgeToEdge ? _controller?.SafeAreaPadding ?? default : default; - public Color? SystemBarColor { get; set; } + public override Color? SystemBarColor { get; set; } } diff --git a/src/iOS/Avalonia.iOS/UIKitInputPane.cs b/src/iOS/Avalonia.iOS/UIKitInputPane.cs index 8706398104..e2ca9ed023 100644 --- a/src/iOS/Avalonia.iOS/UIKitInputPane.cs +++ b/src/iOS/Avalonia.iOS/UIKitInputPane.cs @@ -11,7 +11,7 @@ namespace Avalonia.iOS; [UnsupportedOSPlatform("tvos")] [SupportedOSPlatform("maccatalyst")] [SupportedOSPlatform("ios")] -internal sealed class UIKitInputPane : IInputPane +internal sealed class UIKitInputPane : InputPaneBase { public static UIKitInputPane Instance { get; } = new(); @@ -24,11 +24,7 @@ internal sealed class UIKitInputPane : IInputPane .DefaultCenter .AddObserver(UIKeyboard.WillHideNotification, KeyboardDownNotification); } - - public InputPaneState State { get; private set; } - public Rect OccludedRect { get; private set; } - public event EventHandler? StateChanged; - + private void KeyboardDownNotification(NSNotification obj) => RaiseEventFromNotification(false, obj); private void KeyboardUpNotification(NSNotification obj) => RaiseEventFromNotification(true, obj); @@ -38,7 +34,7 @@ internal sealed class UIKitInputPane : IInputPane State = isUp ? InputPaneState.Open : InputPaneState.Closed; #if MACCATALYST OccludedRect = default; - StateChanged?.Invoke(this, new InputPaneStateEventArgs( + OnStateChanged(new InputPaneStateEventArgs( State, null, OccludedRect)); #else var startFrame = UIKeyboard.FrameBeginFromNotification(notification); @@ -55,7 +51,7 @@ internal sealed class UIKitInputPane : IInputPane var startRect = new Rect(startFrame.X, startFrame.Y, startFrame.Width, startFrame.Height); OccludedRect = new Rect(endFrame.X, endFrame.Y, endFrame.Width, endFrame.Height); - StateChanged?.Invoke(this, new InputPaneStateEventArgs( + OnStateChanged(new InputPaneStateEventArgs( State, startRect, OccludedRect, TimeSpan.FromSeconds(duration), easing)); #endif }