diff --git a/src/iOS/Avalonia.iOS/DispatcherImpl.cs b/src/iOS/Avalonia.iOS/DispatcherImpl.cs new file mode 100644 index 0000000000..8b3c747b5a --- /dev/null +++ b/src/iOS/Avalonia.iOS/DispatcherImpl.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.Threading; +using Avalonia.Threading; +using CoreFoundation; +using Foundation; +using ObjCRuntime; +using CFIndex = System.IntPtr; + +namespace Avalonia.iOS; + +internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing +{ + // CFRunLoopTimerSetNextFireDate docs recommend to "create a repeating timer with an initial + // firing time in the distant future (or the initial firing time) and a very large repeat + // interval—on the order of decades or more" + private const double DistantFutureInterval = (double)50 * 365 * 24 * 3600; + internal static readonly DispatcherImpl Instance = new(); + + private readonly Stopwatch _clock = Stopwatch.StartNew(); + private readonly Action _checkSignaledAction; + private readonly Action _wakeUpLoopAction; + private readonly IntPtr _timer; + private Thread? _loopThread; + private bool _backgroundProcessingRequested, _signaled; + + private DispatcherImpl() + { + _checkSignaledAction = CheckSignaled; + _wakeUpLoopAction = () => + { + // This is needed to wakeup the loop if we are called from inside of BeforeWait hook + }; + + var observer = Interop.CFRunLoopObserverCreate(IntPtr.Zero, + Interop.CFOptionFlags.kCFRunLoopAfterWaiting | Interop.CFOptionFlags.kCFRunLoopBeforeSources | + Interop.CFOptionFlags.kCFRunLoopBeforeWaiting, + true, 0, ObserverCallback, IntPtr.Zero); + Interop.CFRunLoopAddObserver(Interop.CFRunLoopGetMain(), observer, Interop.kCFRunLoopCommonModes); + + _timer = Interop.CFRunLoopTimerCreate(IntPtr.Zero, + Interop.CFAbsoluteTimeGetCurrent() + DistantFutureInterval, + DistantFutureInterval, 0, 0, TimerCallback, IntPtr.Zero); + Interop.CFRunLoopAddTimer(Interop.CFRunLoopGetMain(), _timer, Interop.kCFRunLoopCommonModes); + } + + public event Action? Signaled; + public event Action? Timer; + public event Action? ReadyForBackgroundProcessing; + + public bool CurrentThreadIsLoopThread + { + get + { + if (_loopThread != null) + return Thread.CurrentThread == _loopThread; + if (!NSThread.IsMain) + return false; + _loopThread = Thread.CurrentThread; + return true; + } + } + + public void Signal() + { + lock (this) + { + if (_signaled) + return; + _signaled = true; + + DispatchQueue.MainQueue.DispatchAsync(_checkSignaledAction); + CFRunLoop.Main.WakeUp(); + } + } + + public void UpdateTimer(long? dueTimeInMs) + { + var ms = dueTimeInMs == null ? -1 : (int)Math.Min(int.MaxValue - 10, Math.Max(1, dueTimeInMs.Value - Now)); + var interval = ms < 0 ? DistantFutureInterval : ((double)ms / 1000); + Interop.CFRunLoopTimerSetTolerance(_timer, 0); + Interop.CFRunLoopTimerSetNextFireDate(_timer, Interop.CFAbsoluteTimeGetCurrent() + interval); + } + + public long Now => _clock.ElapsedMilliseconds; + + public void RequestBackgroundProcessing() + { + if (_backgroundProcessingRequested) + return; + _backgroundProcessingRequested = true; + DispatchQueue.MainQueue.DispatchAsync(_wakeUpLoopAction); + } + + private void CheckSignaled() + { + bool signaled; + lock (this) + { + signaled = _signaled; + _signaled = false; + } + + if (signaled) + { + Signaled?.Invoke(); + } + } + + [MonoPInvokeCallback(typeof(Interop.CFRunLoopObserverCallback))] + private static void ObserverCallback(IntPtr observer, Interop.CFOptionFlags activity, IntPtr info) + { + if (activity == Interop.CFOptionFlags.kCFRunLoopBeforeWaiting) + { + bool triggerProcessing; + lock (Instance) + { + triggerProcessing = Instance._backgroundProcessingRequested; + Instance._backgroundProcessingRequested = false; + } + + if (triggerProcessing) Instance.ReadyForBackgroundProcessing?.Invoke(); + } + + Instance.CheckSignaled(); + } + + [MonoPInvokeCallback(typeof(Interop.CFRunLoopTimerCallback))] + private static void TimerCallback(IntPtr timer, IntPtr info) + { + Instance.Timer?.Invoke(); + } +} diff --git a/src/iOS/Avalonia.iOS/Interop.cs b/src/iOS/Avalonia.iOS/Interop.cs new file mode 100644 index 0000000000..c0b3506936 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Interop.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using CoreFoundation; +using Foundation; +using ObjCRuntime; + +namespace Avalonia.iOS; + +// TODO: use LibraryImport in NET7 +internal class Interop +{ + internal const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + internal static NativeHandle kCFRunLoopCommonModes = CFString.CreateNative("kCFRunLoopCommonModes"); + + [Flags] + internal enum CFOptionFlags : ulong + { + kCFRunLoopBeforeSources = (1UL << 2), + kCFRunLoopAfterWaiting = (1UL << 6), + kCFRunLoopBeforeWaiting = (1UL << 5) + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate void CFRunLoopObserverCallback(IntPtr observer, CFOptionFlags activity, IntPtr info); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate void CFRunLoopTimerCallback(IntPtr timer, IntPtr info); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopGetMain(); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopObserverCreate(IntPtr allocator, CFOptionFlags activities, + bool repeats, int index, CFRunLoopObserverCallback callout, IntPtr context); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopAddObserver(IntPtr loop, IntPtr observer, IntPtr mode); + + [DllImport(CoreFoundationLibrary)] + internal static extern IntPtr CFRunLoopTimerCreate(IntPtr allocator, double firstDate, double interval, + CFOptionFlags flags, int order, CFRunLoopTimerCallback callout, IntPtr context); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFRunLoopTimerSetTolerance(IntPtr timer, double tolerance); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFRunLoopTimerSetNextFireDate(IntPtr timer, double fireDate); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFRunLoopAddTimer(IntPtr loop, IntPtr timer, IntPtr mode); + + [DllImport(CoreFoundationLibrary)] + internal static extern double CFAbsoluteTimeGetCurrent(); +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 9187187648..cf5c89cf5f 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -7,6 +7,7 @@ using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Threading; namespace Avalonia { @@ -44,7 +45,7 @@ namespace Avalonia.iOS .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(Timer) - .Bind().ToConstant(new PlatformThreadingInterface()) + .Bind().ToConstant(DispatcherImpl.Instance) .Bind().ToConstant(keyboard); Compositor = new Compositor( diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs index 082966f5b2..07e366e79e 100644 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -31,24 +31,25 @@ internal class PlatformSettings : DefaultPlatformSettings if (tintColor is not null) { tintColor.GetRGBA(out var red, out var green, out var blue, out var alpha); - return _lastColorValues = new PlatformColorValues + if (red != 0 && green != 0 && blue != 0 && alpha != 0) { - ThemeVariant = themeVariant, - ContrastPreference = contrastPreference, - AccentColor1 = new Color( - (byte)(alpha * 255), - (byte)(red * 255), - (byte)(green * 255), - (byte)(blue * 255)) - }; + return _lastColorValues = new PlatformColorValues + { + ThemeVariant = themeVariant, + ContrastPreference = contrastPreference, + AccentColor1 = new Color( + (byte)(alpha * 255), + (byte)(red * 255), + (byte)(green * 255), + (byte)(blue * 255)) + }; + } } - else + + return _lastColorValues = new PlatformColorValues { - return _lastColorValues = new PlatformColorValues - { - ThemeVariant = themeVariant, ContrastPreference = contrastPreference - }; - } + ThemeVariant = themeVariant, ContrastPreference = contrastPreference + }; } public void TraitCollectionDidChange() diff --git a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs b/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs deleted file mode 100644 index 93caecf711..0000000000 --- a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Threading; -using Avalonia.Platform; -using Avalonia.Threading; -using CoreFoundation; -using Foundation; - -namespace Avalonia.iOS -{ - class PlatformThreadingInterface : IPlatformThreadingInterface - { - private bool _signaled; - public static PlatformThreadingInterface Instance { get; } = new PlatformThreadingInterface(); - public bool CurrentThreadIsLoopThread => NSThread.Current.IsMainThread; - - public event Action Signaled; - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - => NSTimer.CreateRepeatingScheduledTimer(interval, _ => tick()); - - public void Signal(DispatcherPriority prio) - { - lock (this) - { - if(_signaled) - return; - _signaled = true; - } - - DispatchQueue.MainQueue.DispatchAsync(() => - { - lock (this) - _signaled = false; - Signaled?.Invoke(null); - }); - } - } -} \ No newline at end of file