浏览代码

Merge pull request #10691 from AvaloniaUI/feature/wpf-like-dispatcher

Implemented dispatcher that works like WPF one
Max Katz 2 年之前
父节点
当前提交
8a9607d5fe
共有 67 个文件被更改,包括 3411 次插入1501 次删除
  1. 4 3
      Avalonia.Desktop.slnf
  2. 207 134
      native/Avalonia.Native/src/OSX/platformthreading.mm
  3. 0 2
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  4. 0 2
      src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs
  5. 2 1
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  6. 1 1
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  7. 553 0
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  8. 238 0
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  9. 207 0
      src/Avalonia.Base/Threading/Dispatcher.Timers.cs
  10. 98 136
      src/Avalonia.Base/Threading/Dispatcher.cs
  11. 308 0
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  12. 31 7
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  13. 418 0
      src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs
  14. 284 139
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  15. 0 25
      src/Avalonia.Base/Threading/IDispatcher.cs
  16. 103 0
      src/Avalonia.Base/Threading/IDispatcherImpl.cs
  17. 0 300
      src/Avalonia.Base/Threading/JobRunner.cs
  18. 0 92
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  19. 109 0
      src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
  20. 2 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  21. 1 1
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  22. 0 86
      src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs
  23. 3 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  24. 3 4
      src/Avalonia.Native/CallbackBase.cs
  25. 132 0
      src/Avalonia.Native/DispatcherImpl.cs
  26. 0 115
      src/Avalonia.Native/PlatformThreadingInterface.cs
  27. 9 7
      src/Avalonia.Native/avn.idl
  28. 3 1
      src/Avalonia.X11/X11Platform.cs
  29. 59 100
      src/Avalonia.X11/X11PlatformThreading.cs
  30. 1 2
      src/Avalonia.X11/X11Window.cs
  31. 0 5
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  32. 7 2
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  33. 3 8
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
  34. 1 3
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  35. 5 7
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  36. 87 22
      src/Shared/RawEventGrouping.cs
  37. 43 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  38. 121 0
      src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs
  39. 16 83
      src/Windows/Avalonia.Win32/Win32Platform.cs
  40. 0 5
      src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs
  41. 10 10
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  42. 3 3
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  43. 21 27
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs
  44. 12 1
      tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
  45. 173 0
      tests/Avalonia.Base.UnitTests/DispatcherTests.cs
  46. 55 73
      tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs
  47. 4 3
      tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
  48. 2 1
      tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
  49. 2 3
      tests/Avalonia.Base.UnitTests/Rendering/RenderLoopTests.cs
  50. 1 1
      tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs
  51. 7 11
      tests/Avalonia.Benchmarks/NullThreadingPlatform.cs
  52. 1 1
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs
  53. 1 1
      tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs
  54. 1 1
      tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs
  55. 1 1
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  56. 1 1
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  57. 6 2
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
  58. 3 7
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  59. 5 18
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
  60. 1 0
      tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj
  61. 1 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  62. 2 0
      tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs
  63. 14 29
      tests/Avalonia.RenderTests/TestBase.cs
  64. 1 0
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  65. 13 0
      tests/Avalonia.UnitTests/DispatcherTimerHelper.cs
  66. 8 7
      tests/Avalonia.UnitTests/TestServices.cs
  67. 3 3
      tests/Avalonia.UnitTests/UnitTestApplication.cs

+ 4 - 3
Avalonia.Desktop.slnf

@@ -38,13 +38,14 @@
       "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
       "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
       "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
-      "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
-      "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
-      "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
+      "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
       "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
       "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
       "src\\tools\\DevGenerators\\DevGenerators.csproj",
       "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
+      "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
+      "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
+      "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
       "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
       "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
       "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",

+ 207 - 134
native/Avalonia.Native/src/OSX/platformthreading.mm

@@ -1,193 +1,266 @@
 #include "common.h"
 
 class PlatformThreadingInterface;
+
+
+class LoopCancellation : public ComSingleObject<IAvnLoopCancellation, &IID_IAvnLoopCancellation>
+{
+public:
+    FORWARD_IUNKNOWN()
+    
+    bool Running = false;
+    bool Cancelled = false;
+    bool IsApp = false;
+
+    virtual void Cancel() override
+    {
+        Cancelled = true;
+        if(Running)
+        {
+            Running = false;
+            if(![NSThread isMainThread])
+            {
+                AddRef();
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    if(Release() == 0)
+                        return;
+                    Cancel();
+                });
+                return;
+            };
+            if(IsApp)
+                [NSApp stop:nil];
+            else
+            {
+                // Wakeup the event loop
+                NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
+                                                    location:NSMakePoint(0, 0)
+                                               modifierFlags:0
+                                                   timestamp:0
+                                                windowNumber:0
+                                                     context:nil
+                                                     subtype:0
+                                                       data1:0
+                                                       data2:0];
+                [NSApp postEvent:event atStart:YES];
+            }
+        }
+    };
+};
+
+// 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"
+static double distantFutureInterval = (double)50*365*24*3600;
+
 @interface Signaler : NSObject
--(void) setParent: (PlatformThreadingInterface*)parent;
--(void) signal: (int) priority;
+-(void) setEvents:(IAvnPlatformThreadingInterfaceEvents*) events;
+-(void) updateTimer:(int)ms;
 -(Signaler*) init;
+-(void) destroyObserver;
+-(void) signal;
 @end
 
-@implementation ActionCallback
+@implementation Signaler
 {
-    ComPtr<IAvnActionCallback> _callback;
+    ComPtr<IAvnPlatformThreadingInterfaceEvents> _events;
+    bool _wakeupDelegateSent;
+    bool _signaled;
+    bool _backgroundProcessingRequested;
+    CFRunLoopObserverRef _observer;
+    CFRunLoopTimerRef _timer;
+}
 
+- (void) checkSignaled
+{
+    bool signaled;
+    @synchronized (self) {
+        signaled = _signaled;
+        _signaled = false;
+    }
+    if(signaled)
+    {
+        _events->Signaled();
+    }
 }
-- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback
+
+- (Signaler*) init
 {
-    _callback = callback;
+    _observer = CFRunLoopObserverCreateWithHandler(nil,
+                                                   kCFRunLoopBeforeSources
+                                                   | kCFRunLoopAfterWaiting
+                                                   | kCFRunLoopBeforeWaiting
+                                                   ,
+                                                   true, 0,
+                                                   ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
+        if(activity == kCFRunLoopBeforeWaiting)
+        {
+            bool triggerProcessing;
+            @synchronized (self) {
+                triggerProcessing = self->_backgroundProcessingRequested;
+                self->_backgroundProcessingRequested = false;
+            }
+            if(triggerProcessing)
+                self->_events->ReadyForBackgroundProcessing();
+        }
+        [self checkSignaled];
+    });
+    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
+    
+    
+    _timer = CFRunLoopTimerCreateWithHandler(nil, CFAbsoluteTimeGetCurrent() + distantFutureInterval, distantFutureInterval, 0, 0, ^(CFRunLoopTimerRef timer) {
+        self->_events->Timer();
+    });
+    
+    CFRunLoopAddTimer(CFRunLoopGetMain(), _timer, kCFRunLoopCommonModes);
+    
     return self;
 }
 
-- (void) action
+- (void) destroyObserver
 {
-    _callback->Run();
+    if(_observer != nil)
+    {
+        CFRunLoopObserverInvalidate(_observer);
+        CFRelease(_observer);
+        _observer = nil;
+    }
+    
+    if(_timer != nil)
+    {
+        CFRunLoopTimerInvalidate(_timer);
+        CFRelease(_timer);
+        _timer = nil;
+    }
 }
 
+-(void) updateTimer:(int)ms
+{
+    if(_timer == nil)
+        return;
+    double interval = ms < 0 ? distantFutureInterval : ((double)ms / 1000);
+    CFRunLoopTimerSetTolerance(_timer, 0);
+    CFRunLoopTimerSetNextFireDate(_timer, CFAbsoluteTimeGetCurrent() + interval);
+}
 
-@end
+- (void) setEvents: (IAvnPlatformThreadingInterfaceEvents*) events
+{
+    _events = events;
+}
 
-class TimerWrapper : public ComUnknownObject
+- (void) signal
 {
-    NSTimer* _timer;
-public:
-    TimerWrapper(IAvnActionCallback* callback, int ms)
-    {
-        auto cb = [[ActionCallback alloc] initWithCallback:callback];
-        _timer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(double)ms/1000 target:cb selector:@selector(action) userInfo:nullptr repeats:true];
+    @synchronized (self) {
+        if(_signaled)
+            return;
+        _signaled = true;
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self checkSignaled];
+        });
+        CFRunLoopWakeUp(CFRunLoopGetMain());
     }
-                  
-    virtual ~TimerWrapper()
-    {
-         [_timer invalidate];
+}
+
+- (void) requestBackgroundProcessing
+{
+    @synchronized (self) {
+        if(_backgroundProcessingRequested)
+            return;
+        _backgroundProcessingRequested = true;
+        dispatch_async(dispatch_get_main_queue(), ^{
+            // This is needed to wakeup the loop if we are called from inside of BeforeWait hook
+        });
     }
-};
+    
+        
+}
 
+@end
 
 
 class PlatformThreadingInterface : public ComSingleObject<IAvnPlatformThreadingInterface, &IID_IAvnPlatformThreadingInterface>
 {
 private:
+    ComPtr<IAvnPlatformThreadingInterfaceEvents> _events;
     Signaler* _signaler;
-    bool _wasRunningAtLeastOnce = false;
-    
-    class LoopCancellation : public ComSingleObject<IAvnLoopCancellation, &IID_IAvnLoopCancellation>
-    {
-    public:
-        FORWARD_IUNKNOWN()
-        
-        bool Running = false;
-        bool Cancelled = false;
-        
-        virtual void Cancel() override
-        {
-            Cancelled = true;
-            if(Running)
-            {
-                Running = false;
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [[NSApplication sharedApplication] stop:nil];
-                    NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
-                                                        location:NSMakePoint(0, 0)
-                                                   modifierFlags:0
-                                                       timestamp:0
-                                                    windowNumber:0
-                                                         context:nil
-                                                         subtype:0
-                                                           data1:0
-                                                           data2:0];
-                    [NSApp postEvent:event atStart:YES];
-                });
-            }
-        }
-
-    };
-    
+    CFRunLoopObserverRef _observer = nil;
 public:
     FORWARD_IUNKNOWN()
-    ComPtr<IAvnSignaledCallback> SignaledCallback;
-
     PlatformThreadingInterface()
     {
         _signaler = [Signaler new];
-        [_signaler setParent:this];
-    }
+    };
     
     ~PlatformThreadingInterface()
     {
-        if(_signaler)
-            [_signaler setParent: NULL];
-        _signaler = NULL;
+        [_signaler destroyObserver];
     }
     
-    virtual bool GetCurrentThreadIsLoopThread() override
+    bool GetCurrentThreadIsLoopThread() override
     {
         return [NSThread isMainThread];
-    }
-    virtual void SetSignaledCallback(IAvnSignaledCallback* cb) override
+    };
+    
+    
+    
+    void SetEvents(IAvnPlatformThreadingInterfaceEvents *cb) override
     {
-        SignaledCallback = cb;
-    }
-    virtual IAvnLoopCancellation* CreateLoopCancellation() override
+        _events = cb;
+        [_signaler setEvents:cb];
+    };
+    
+    IAvnLoopCancellation *CreateLoopCancellation() override
     {
         return new LoopCancellation();
-    }
+    };
     
-    virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override
+    void RunLoop(IAvnLoopCancellation *cancel) override
     {
         START_COM_CALL;
-        
         auto can = dynamic_cast<LoopCancellation*>(cancel);
         if(can->Cancelled)
-            return S_OK;
-        if(_wasRunningAtLeastOnce)
-            return E_FAIL;
+            return;
         can->Running = true;
-        _wasRunningAtLeastOnce = true;
-        [NSApp run];
-        return S_OK;
-    }
+        if(![NSApp isRunning])
+        {
+            can->IsApp = true;
+            [NSApp run];
+            return;
+        }
+        else
+        {
+            while(!can->Cancelled)
+            {
+                @autoreleasepool
+                {
+                    NSEvent* ev = [NSApp
+                                   nextEventMatchingMask:NSEventMaskAny
+                                   untilDate: [NSDate dateWithTimeIntervalSinceNow:1]
+                                   inMode:NSDefaultRunLoopMode
+                                   dequeue:true];
+                    if(ev != NULL)
+                        [NSApp sendEvent:ev];
+                }
+            }
+        }
+    };
     
-    virtual void Signal(int priority) override
+    void Signal() override
     {
-        [_signaler signal:priority];
-    }
+        [_signaler signal];
+    };
     
-    virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) override
-    {
-        @autoreleasepool {
-            
-            return new TimerWrapper(callback, ms);
-        }
-    }
-};
-
-@implementation Signaler
-
-PlatformThreadingInterface* _parent = 0;
-bool _signaled = 0;
-NSArray<NSString*>* _modes;
-
--(Signaler*) init
-{
-    if(self = [super init])
+    void UpdateTimer(int ms) override
     {
-        _modes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil];
-    }
-    return self;
-}
-
--(void) perform
-{
-    ComPtr<IAvnSignaledCallback> cb;
-    @synchronized (self) {
-        _signaled  = false;
-        if(_parent != NULL)
-            cb = _parent->SignaledCallback;
-    }
-    if(cb != nullptr)
-        cb->Signaled(0, false);
-}
-
--(void) setParent:(PlatformThreadingInterface *)parent
-{
-    @synchronized (self) {
-        _parent = parent;
-    }
-}
-
--(void) signal: (int) priority
-{
-
-    @synchronized (self) {
-        if(_signaled)
-            return;
-        _signaled = true;
-        [self performSelector:@selector(perform) onThread:[NSThread mainThread] withObject:NULL waitUntilDone:false modes:_modes];
+        [_signaler updateTimer:ms];
+    };
+    
+    void RequestBackgroundProcessing() override {
+        [_signaler requestBackgroundProcessing];
     }
     
-}
-@end
-
+    
+};
 
 extern IAvnPlatformThreadingInterface* CreatePlatformThreading()
 {

+ 0 - 2
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@@ -21,8 +21,6 @@ namespace Avalonia.Android
             _handler = new Handler(App.Context.MainLooper);
         }
 
-        public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
-
         public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
         {
             if (interval.TotalMilliseconds < 10)

+ 0 - 2
src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs

@@ -11,8 +11,6 @@ namespace Avalonia.Platform
     [Unstable]
     public interface IPlatformThreadingInterface
     {
-        void RunLoop(CancellationToken cancellationToken);
-
         /// <summary>
         /// Starts a timer.
         /// </summary>

+ 2 - 1
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@@ -30,7 +30,8 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
             GC.SuppressFinalize(needsFinalize);
 
         var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
-        if (AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null)
+        if (AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null
+            && AvaloniaLocator.Current.GetService<IDispatcherImpl>() == null)
             _reclaimImmediately = true;
         else
             StartUpdateTimer(startTimer, updateRef);

+ 1 - 1
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Threading
         /// <inheritdoc/>
         public override void Post(SendOrPostCallback d, object? state)
         {
-           Dispatcher.UIThread.Post(d, state, DispatcherPriority.Background);
+            Dispatcher.UIThread.Post(d, state, DispatcherPriority.Background);
         }
 
         /// <inheritdoc/>

+ 553 - 0
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@@ -0,0 +1,553 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Threading;
+
+namespace Avalonia.Threading;
+
+public partial class Dispatcher
+{
+    /// <summary>
+    ///     Executes the specified Action synchronously on the thread that
+    ///     the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     An Action delegate to invoke through the dispatcher.
+    /// </param>
+    /// <remarks>
+    ///     Note that the default priority is DispatcherPriority.Send.
+    /// </remarks>
+    public void Invoke(Action callback)
+    {
+        Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1));
+    }
+
+    /// <summary>
+    ///     Executes the specified Action synchronously on the thread that
+    ///     the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     An Action delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    public void Invoke(Action callback, DispatcherPriority priority)
+    {
+        Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1));
+    }
+
+    /// <summary>
+    ///     Executes the specified Action synchronously on the thread that
+    ///     the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     An Action delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <param name="cancellationToken">
+    ///     A cancellation token that can be used to cancel the operation.
+    ///     If the operation has not started, it will be aborted when the
+    ///     cancellation token is canceled.  If the operation has started,
+    ///     the operation can cooperate with the cancellation request.
+    /// </param>
+    public void Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken)
+    {
+        Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1));
+    }
+
+    /// <summary>
+    ///     Executes the specified Action synchronously on the thread that
+    ///     the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     An Action delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <param name="cancellationToken">
+    ///     A cancellation token that can be used to cancel the operation.
+    ///     If the operation has not started, it will be aborted when the
+    ///     cancellation token is canceled.  If the operation has started,
+    ///     the operation can cooperate with the cancellation request.
+    /// </param>
+    /// <param name="timeout">
+    ///     The minimum amount of time to wait for the operation to start.
+    ///     Once the operation has started, it will complete before this method
+    ///     returns.
+    /// </param>
+    public void Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken,
+        TimeSpan timeout)
+    {
+        if (callback == null)
+        {
+            throw new ArgumentNullException("callback");
+        }
+
+        DispatcherPriority.Validate(priority, "priority");
+
+        if (timeout.TotalMilliseconds < 0 &&
+            timeout != TimeSpan.FromMilliseconds(-1))
+        {
+            throw new ArgumentOutOfRangeException("timeout");
+        }
+
+        // Fast-Path: if on the same thread, and invoking at Send priority,
+        // and the cancellation token is not already canceled, then just
+        // call the callback directly.
+        if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess())
+        {
+            callback();
+            return;
+        }
+
+        // Slow-Path: go through the queue.
+        DispatcherOperation operation = new DispatcherOperation(this, priority, callback, false);
+        InvokeImpl(operation, cancellationToken, timeout);
+    }
+
+    /// <summary>
+    ///     Executes the specified Func<TResult> synchronously on the
+    ///     thread that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     A Func<TResult> delegate to invoke through the dispatcher.
+    /// </param>
+    /// <returns>
+    ///     The return value from the delegate being invoked.
+    /// </returns>
+    /// <remarks>
+    ///     Note that the default priority is DispatcherPriority.Send.
+    /// </remarks>
+    public TResult Invoke<TResult>(Func<TResult> callback)
+    {
+        return Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1));
+    }
+
+    /// <summary>
+    ///     Executes the specified Func<TResult> synchronously on the
+    ///     thread that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     A Func<TResult> delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <returns>
+    ///     The return value from the delegate being invoked.
+    /// </returns>
+    public TResult Invoke<TResult>(Func<TResult> callback, DispatcherPriority priority)
+    {
+        return Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1));
+    }
+
+    /// <summary>
+    ///     Executes the specified Func<TResult> synchronously on the
+    ///     thread that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     A Func<TResult> delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <param name="cancellationToken">
+    ///     A cancellation token that can be used to cancel the operation.
+    ///     If the operation has not started, it will be aborted when the
+    ///     cancellation token is canceled.  If the operation has started,
+    ///     the operation can cooperate with the cancellation request.
+    /// </param>
+    /// <returns>
+    ///     The return value from the delegate being invoked.
+    /// </returns>
+    public TResult Invoke<TResult>(Func<TResult> callback, DispatcherPriority priority,
+        CancellationToken cancellationToken)
+    {
+        return Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1));
+    }
+
+    /// <summary>
+    ///     Executes the specified Func<TResult> synchronously on the
+    ///     thread that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     A Func<TResult> delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <param name="cancellationToken">
+    ///     A cancellation token that can be used to cancel the operation.
+    ///     If the operation has not started, it will be aborted when the
+    ///     cancellation token is canceled.  If the operation has started,
+    ///     the operation can cooperate with the cancellation request.
+    /// </param>
+    /// <param name="timeout">
+    ///     The minimum amount of time to wait for the operation to start.
+    ///     Once the operation has started, it will complete before this method
+    ///     returns.
+    /// </param>
+    /// <returns>
+    ///     The return value from the delegate being invoked.
+    /// </returns>
+    public TResult Invoke<TResult>(Func<TResult> callback, DispatcherPriority priority,
+        CancellationToken cancellationToken, TimeSpan timeout)
+    {
+        if (callback == null)
+        {
+            throw new ArgumentNullException("callback");
+        }
+
+        DispatcherPriority.Validate(priority, "priority");
+
+        if (timeout.TotalMilliseconds < 0 &&
+            timeout != TimeSpan.FromMilliseconds(-1))
+        {
+            throw new ArgumentOutOfRangeException("timeout");
+        }
+
+        // Fast-Path: if on the same thread, and invoking at Send priority,
+        // and the cancellation token is not already canceled, then just
+        // call the callback directly.
+        if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess())
+        {
+            return callback();
+        }
+
+        // Slow-Path: go through the queue.
+        DispatcherOperation<TResult> operation = new DispatcherOperation<TResult>(this, priority, callback);
+        return (TResult)InvokeImpl(operation, cancellationToken, timeout)!;
+    }
+
+    /// <summary>
+    ///     Executes the specified Action asynchronously on the thread
+    ///     that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     An Action delegate to invoke through the dispatcher.
+    /// </param>
+    /// <returns>
+    ///     An operation representing the queued delegate to be invoked.
+    /// </returns>
+    /// <remarks>
+    ///     Note that the default priority is DispatcherPriority.Normal.
+    /// </remarks>
+    public DispatcherOperation InvokeAsync(Action callback)
+    {
+        return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None);
+    }
+
+    /// <summary>
+    ///     Executes the specified Action asynchronously on the thread
+    ///     that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     An Action delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <returns>
+    ///     An operation representing the queued delegate to be invoked.
+    /// </returns>
+    /// <returns>
+    ///     An operation representing the queued delegate to be invoked.
+    /// </returns>
+    public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority)
+    {
+        return InvokeAsync(callback, priority, CancellationToken.None);
+    }
+
+    /// <summary>
+    ///     Executes the specified Action asynchronously on the thread
+    ///     that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     An Action delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <param name="cancellationToken">
+    ///     A cancellation token that can be used to cancel the operation.
+    ///     If the operation has not started, it will be aborted when the
+    ///     cancellation token is canceled.  If the operation has started,
+    ///     the operation can cooperate with the cancellation request.
+    /// </param>
+    /// <returns>
+    ///     An operation representing the queued delegate to be invoked.
+    /// </returns>
+    public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority,
+        CancellationToken cancellationToken)
+    {
+        if (callback == null)
+        {
+            throw new ArgumentNullException("callback");
+        }
+
+        DispatcherPriority.Validate(priority, "priority");
+
+        DispatcherOperation operation = new DispatcherOperation(this, priority, callback, false);
+        InvokeAsyncImpl(operation, cancellationToken);
+
+        return operation;
+    }
+
+    /// <summary>
+    ///     Executes the specified Func<TResult> asynchronously on the
+    ///     thread that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     A Func<TResult> delegate to invoke through the dispatcher.
+    /// </param>
+    /// <returns>
+    ///     An operation representing the queued delegate to be invoked.
+    /// </returns>
+    /// <remarks>
+    ///     Note that the default priority is DispatcherPriority.Normal.
+    /// </remarks>
+    public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback)
+    {
+        return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None);
+    }
+
+    /// <summary>
+    ///     Executes the specified Func<TResult> asynchronously on the
+    ///     thread that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     A Func<TResult> delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <returns>
+    ///     An operation representing the queued delegate to be invoked.
+    /// </returns>
+    public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback, DispatcherPriority priority)
+    {
+        return InvokeAsync(callback, priority, CancellationToken.None);
+    }
+
+    /// <summary>
+    ///     Executes the specified Func<TResult> asynchronously on the
+    ///     thread that the Dispatcher was created on.
+    /// </summary>
+    /// <param name="callback">
+    ///     A Func<TResult> delegate to invoke through the dispatcher.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority that determines in what order the specified
+    ///     callback is invoked relative to the other pending operations
+    ///     in the Dispatcher.
+    /// </param>
+    /// <param name="cancellationToken">
+    ///     A cancellation token that can be used to cancel the operation.
+    ///     If the operation has not started, it will be aborted when the
+    ///     cancellation token is canceled.  If the operation has started,
+    ///     the operation can cooperate with the cancellation request.
+    /// </param>
+    /// <returns>
+    ///     An operation representing the queued delegate to be invoked.
+    /// </returns>
+    public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback, DispatcherPriority priority,
+        CancellationToken cancellationToken)
+    {
+        if (callback == null)
+        {
+            throw new ArgumentNullException("callback");
+        }
+
+        DispatcherPriority.Validate(priority, "priority");
+
+        DispatcherOperation<TResult> operation = new DispatcherOperation<TResult>(this, priority, callback);
+        InvokeAsyncImpl(operation, cancellationToken);
+
+        return operation;
+    }
+
+    private void InvokeAsyncImpl(DispatcherOperation operation, CancellationToken cancellationToken)
+    {
+        bool succeeded = false;
+
+        // Could be a non-dispatcher thread, lock to read
+        lock (InstanceLock)
+        {
+            if (!cancellationToken.IsCancellationRequested &&
+                !_hasShutdownFinished &&
+                !Environment.HasShutdownStarted)
+            {
+                // Add the operation to the work queue
+                _queue.Enqueue(operation.Priority, operation);
+
+                // Make sure we will wake up to process this operation.
+                succeeded = RequestProcessing();
+
+                if (!succeeded)
+                {
+                    // Dequeue the item since we failed to request
+                    // processing for it.  Note we will mark it aborted
+                    // below.
+                    _queue.RemoveItem(operation);
+                }
+            }
+        }
+
+        if (succeeded == true)
+        {
+            // We have enqueued the operation.  Register a callback
+            // with the cancellation token to abort the operation
+            // when cancellation is requested.
+            if (cancellationToken.CanBeCanceled)
+            {
+                CancellationTokenRegistration cancellationRegistration =
+                    cancellationToken.Register(s => ((DispatcherOperation)s!).Abort(), operation);
+
+                // Revoke the cancellation when the operation is done.
+                operation.Aborted += (s, e) => cancellationRegistration.Dispose();
+                operation.Completed += (s, e) => cancellationRegistration.Dispose();
+            }
+        }
+        else
+        {
+            // We failed to enqueue the operation, and the caller that
+            // created the operation does not expose it before we return,
+            // so it is safe to modify the operation outside of the lock.
+            // Just mark the operation as aborted, which we can safely
+            // return to the user.
+            operation.DoAbort();
+        }
+    }
+
+
+    private object? InvokeImpl(DispatcherOperation operation, CancellationToken cancellationToken, TimeSpan timeout)
+    {
+        object? result = null;
+
+        Debug.Assert(timeout.TotalMilliseconds >= 0 || timeout == TimeSpan.FromMilliseconds(-1));
+        Debug.Assert(operation.Priority != DispatcherPriority.Send || !CheckAccess()); // should be handled by caller
+
+        if (!cancellationToken.IsCancellationRequested)
+        {
+            // This operation must be queued since it was invoked either to
+            // another thread, or at a priority other than Send.
+            InvokeAsyncImpl(operation, cancellationToken);
+
+            CancellationToken ctTimeout = CancellationToken.None;
+            CancellationTokenRegistration ctTimeoutRegistration = new CancellationTokenRegistration();
+            CancellationTokenSource? ctsTimeout = null;
+
+            if (timeout.TotalMilliseconds >= 0)
+            {
+                // Create a CancellationTokenSource that will abort the
+                // operation after the timeout.  Note that this does not
+                // cancel the operation, just abort it if it is still pending.
+                ctsTimeout = new CancellationTokenSource(timeout);
+                ctTimeout = ctsTimeout.Token;
+                ctTimeoutRegistration = ctTimeout.Register(s => ((DispatcherOperation)s!).Abort(), operation);
+            }
+
+
+            // We have already registered with the cancellation tokens
+            // (both provided by the user, and one for the timeout) to
+            // abort the operation when they are canceled.  If the
+            // operation has already started when the timeout expires,
+            // we still wait for it to complete.  This is different
+            // than simply waiting on the operation with a timeout
+            // because we are the ones queueing the dispatcher
+            // operation, not the caller.  We can't leave the operation
+            // in a state that it might execute if we return that it did not
+            // invoke.
+            try
+            {
+                operation.GetTask().Wait();
+
+                Debug.Assert(operation.Status == DispatcherOperationStatus.Completed ||
+                             operation.Status == DispatcherOperationStatus.Aborted);
+
+                // Old async semantics return from Wait without
+                // throwing an exception if the operation was aborted.
+                // There is no need to test the timout condition, since
+                // the old async semantics would just return the result,
+                // which would be null.
+
+                // This should not block because either the operation
+                // is using the old async sematics, or the operation
+                // completed successfully.
+                result = operation.GetResult();
+            }
+            catch (OperationCanceledException)
+            {
+                Debug.Assert(operation.Status == DispatcherOperationStatus.Aborted);
+
+                // New async semantics will throw an exception if the
+                // operation was aborted.  Here we convert that
+                // exception into a timeout exception if the timeout
+                // has expired (admittedly a weak relationship
+                // assuming causality).
+                if (ctTimeout.IsCancellationRequested)
+                {
+                    // The operation was canceled because of the
+                    // timeout, throw a TimeoutException instead.
+                    throw new TimeoutException();
+                }
+                else
+                {
+                    // The operation was canceled from some other reason.
+                    throw;
+                }
+            }
+            finally
+            {
+                ctTimeoutRegistration.Dispose();
+                if (ctsTimeout != null)
+                {
+                    ctsTimeout.Dispose();
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /// <inheritdoc/>
+    public void Post(Action action, DispatcherPriority priority = default)
+    {
+        _ = action ?? throw new ArgumentNullException(nameof(action));
+        InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None);
+    }
+
+    /// <summary>
+    /// Posts an action that will be invoked on the dispatcher thread.
+    /// </summary>
+    /// <param name="action">The method.</param>
+    /// <param name="arg">The argument of method to call.</param>
+    /// <param name="priority">The priority with which to invoke the method.</param>
+    public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default)
+    {
+        _ = action ?? throw new ArgumentNullException(nameof(action));
+        InvokeAsyncImpl(new SendOrPostCallbackDispatcherOperation(this, priority, action, arg, true), CancellationToken.None);
+    }
+}

+ 238 - 0
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@@ -0,0 +1,238 @@
+using System;
+using System.Diagnostics;
+
+namespace Avalonia.Threading;
+
+public partial class Dispatcher
+{
+    private readonly DispatcherPriorityQueue _queue = new();
+    private bool _signaled;
+    private bool _explicitBackgroundProcessingRequested;
+    private const int MaximumTimeProcessingBackgroundJobs = 50;
+    
+    void RequestBackgroundProcessing()
+    {
+        lock (InstanceLock)
+        {
+            if (_backgroundProcessingImpl != null)
+            {
+                if(_explicitBackgroundProcessingRequested)
+                    return;
+                _explicitBackgroundProcessingRequested = true;
+                _backgroundProcessingImpl.RequestBackgroundProcessing();
+            }
+            else if (_dueTimeForBackgroundProcessing == null)
+            {
+                _dueTimeForBackgroundProcessing = Now + 1;
+                UpdateOSTimer();
+            }
+        }
+    }
+
+    private void OnReadyForExplicitBackgroundProcessing()
+    {
+        lock (InstanceLock)
+        {
+            _explicitBackgroundProcessingRequested = false;
+            ExecuteJobsCore();
+        }
+    }
+
+    /// <summary>
+    /// Force-runs all dispatcher operations ignoring any pending OS events, use with caution
+    /// </summary>
+    public void RunJobs(DispatcherPriority? priority = null)
+    {
+        priority ??= DispatcherPriority.MinimumActiveValue;
+        if (priority < DispatcherPriority.MinimumActiveValue)
+            priority = DispatcherPriority.MinimumActiveValue;
+        while (true)
+        {
+            DispatcherOperation? job;
+            lock (InstanceLock)
+                job = _queue.Peek();
+            if (job == null)
+                return;
+            if (priority != null && job.Priority < priority.Value)
+                return;
+            ExecuteJob(job);
+        }
+    }
+
+    class DummyShuttingDownUnitTestDispatcherImpl : IDispatcherImpl
+    {
+        public bool CurrentThreadIsLoopThread => true;
+        public void Signal()
+        {
+        }
+
+        public event Action? Signaled;
+        public event Action? Timer;
+        public long Now => 0;
+        public void UpdateTimer(long? dueTimeInMs)
+        {
+        }
+    }
+    
+    internal static void ResetForUnitTests()
+    {
+        if (s_uiThread == null)
+            return;
+        var st = Stopwatch.StartNew();
+        while (true)
+        {
+            s_uiThread._pendingInputImpl = s_uiThread._controlledImpl = null;
+            s_uiThread._impl = new DummyShuttingDownUnitTestDispatcherImpl();
+            if (st.Elapsed.TotalSeconds > 5)
+                throw new InvalidProgramException("You've caused dispatcher loop");
+            
+            DispatcherOperation? job;
+            lock (s_uiThread.InstanceLock)
+                job = s_uiThread._queue.Peek();
+            if (job == null || job.Priority <= DispatcherPriority.Inactive)
+            {
+                s_uiThread = null;
+                return;
+            }
+
+            s_uiThread.ExecuteJob(job);
+        }
+
+    }
+    
+    private void ExecuteJob(DispatcherOperation job)
+    {
+        lock (InstanceLock)
+            _queue.RemoveItem(job);
+        job.Execute();
+        // The backend might be firing timers with a low priority,
+        // so we manually check if our high priority timers are due for execution
+        PromoteTimers();
+    }
+
+    private void Signaled()
+    {
+        lock (InstanceLock)
+            _signaled = false;
+
+        ExecuteJobsCore();
+    }
+
+    void ExecuteJobsCore()
+    {
+        long? backgroundJobExecutionStartedAt = null;
+        while (true)
+        {
+            DispatcherOperation? job;
+
+            lock (InstanceLock)
+                job = _queue.Peek();
+            
+            if (job == null || job.Priority < DispatcherPriority.MinimumActiveValue)
+                return;
+            
+            
+            // We don't stop for executing jobs queued with >Input priority
+            if (job.Priority > DispatcherPriority.Input)
+            {
+                ExecuteJob(job);
+                backgroundJobExecutionStartedAt = null;
+            }
+            // If platform supports pending input query, ask the platform if we can continue running low priority jobs
+            else if (_pendingInputImpl?.CanQueryPendingInput == true)
+            {
+                if (!_pendingInputImpl.HasPendingInput)
+                    ExecuteJob(job);
+                else
+                {
+                    RequestBackgroundProcessing();
+                    return;
+                }
+            }
+            // We can't check if there is pending input, but still need to enforce interactivity
+            // so we stop processing background jobs after some timeout and start a timer to continue later
+            else
+            {
+                if (backgroundJobExecutionStartedAt == null)
+                    backgroundJobExecutionStartedAt = Now;
+                
+                if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs)
+                {
+                    _signaled = true;
+                    RequestBackgroundProcessing();
+                    return;
+                }
+                else
+                    ExecuteJob(job);
+            }
+        }
+    }
+
+    private bool RequestProcessing()
+    {
+        lock (InstanceLock)
+        {
+            if (!CheckAccess())
+            {
+                RequestForegroundProcessing();
+                return true;
+            }
+
+            if (_queue.MaxPriority <= DispatcherPriority.Input)
+            {
+                if (_pendingInputImpl is { CanQueryPendingInput: true, HasPendingInput: false })
+                    RequestForegroundProcessing();
+                else
+                    RequestBackgroundProcessing();
+            }
+            else
+                RequestForegroundProcessing();
+        }
+        return true;
+    }
+
+    private void RequestForegroundProcessing()
+    {
+        if (!_signaled)
+        {
+            _signaled = true;
+            _impl.Signal();
+        }
+    }
+
+    internal void Abort(DispatcherOperation operation)
+    {
+        lock (InstanceLock)
+            _queue.RemoveItem(operation);
+        operation.DoAbort();
+    }
+    
+    // Returns whether or not the priority was set.
+    internal bool SetPriority(DispatcherOperation operation, DispatcherPriority priority) // NOTE: should be Priority
+    {
+        bool notify = false;
+
+        lock(InstanceLock)
+        {
+            if(operation.IsQueued)
+            {
+                _queue.ChangeItemPriority(operation, priority);
+                notify = true;
+
+                if(notify)
+                {
+                    // Make sure we will wake up to process this operation.
+                    RequestProcessing();
+
+                }
+            }
+        }
+        return notify;
+    }
+
+    public bool HasJobsWithPriority(DispatcherPriority priority)
+    {
+        lock (InstanceLock)
+            return _queue.MaxPriority >= priority;
+    }
+}

+ 207 - 0
src/Avalonia.Base/Threading/Dispatcher.Timers.cs

@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Threading;
+
+public partial class Dispatcher
+{
+    private List<DispatcherTimer> _timers = new();
+    private long _timersVersion;
+    private bool _dueTimeFound;
+    private long _dueTimeInMs;
+
+    private long? _dueTimeForTimers;
+    private long? _dueTimeForBackgroundProcessing;
+    private long? _osTimerSetTo;
+
+    internal long Now => _impl.Now;
+
+    private void UpdateOSTimer()
+    {
+        VerifyAccess();
+        var nextDueTime =
+            (_dueTimeForTimers.HasValue && _dueTimeForBackgroundProcessing.HasValue) ?
+                Math.Min(_dueTimeForTimers.Value, _dueTimeForBackgroundProcessing.Value) :
+                _dueTimeForTimers ?? _dueTimeForBackgroundProcessing;
+        if (_osTimerSetTo == nextDueTime)
+            return;
+        _impl.UpdateTimer(_osTimerSetTo = nextDueTime);
+    }
+
+    internal void RescheduleTimers()
+    {
+        if (!CheckAccess())
+        {
+            Post(RescheduleTimers, DispatcherPriority.Send);
+            return;
+        }
+
+        lock (InstanceLock)
+        {
+            if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read
+            {
+                bool oldDueTimeFound = _dueTimeFound;
+                long oldDueTimeInTicks = _dueTimeInMs;
+                _dueTimeFound = false;
+                _dueTimeInMs = 0;
+
+                if (_timers.Count > 0)
+                {
+                    // We could do better if we sorted the list of timers.
+                    for (int i = 0; i < _timers.Count; i++)
+                    {
+                        var timer = _timers[i];
+
+                        if (!_dueTimeFound || timer.DueTimeInMs - _dueTimeInMs < 0)
+                        {
+                            _dueTimeFound = true;
+                            _dueTimeInMs = timer.DueTimeInMs;
+                        }
+                    }
+                }
+
+                if (_dueTimeFound)
+                {
+                    if (_dueTimeForTimers == null || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs))
+                    {
+                        _dueTimeForTimers = _dueTimeInMs;
+                        UpdateOSTimer();
+                    }
+                }
+                else if (oldDueTimeFound)
+                {
+                    _dueTimeForTimers = null;
+                    UpdateOSTimer();
+                }
+            }
+        }
+    }
+
+    internal void AddTimer(DispatcherTimer timer)
+    {
+        lock (InstanceLock)
+        {
+            if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
+            {
+                _timers.Add(timer);
+                _timersVersion++;
+            }
+        }
+
+        RescheduleTimers();
+    }
+
+    internal void RemoveTimer(DispatcherTimer timer)
+    {
+        lock (InstanceLock)
+        {
+            if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
+            {
+                _timers.Remove(timer);
+                _timersVersion++;
+            }
+        }
+
+        RescheduleTimers();
+    }
+
+    private void OnOSTimer()
+    {
+        _impl.UpdateTimer(null);
+        _osTimerSetTo = null;
+        bool needToPromoteTimers = false;
+        bool needToProcessQueue = false;
+        lock (InstanceLock)
+        {
+            _impl.UpdateTimer(_osTimerSetTo = null);
+            needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now;
+            if (needToPromoteTimers)
+                _dueTimeForTimers = null;
+            needToProcessQueue = _dueTimeForBackgroundProcessing.HasValue &&
+                                 _dueTimeForBackgroundProcessing.Value <= Now;
+            if (needToProcessQueue)
+                _dueTimeForBackgroundProcessing = null;
+        }
+
+        if (needToPromoteTimers)
+            PromoteTimers();
+        if (needToProcessQueue)
+            ExecuteJobsCore();
+        UpdateOSTimer();
+    }
+    
+    internal void PromoteTimers()
+    {
+        long currentTimeInTicks = Now;
+        try
+        {
+            List<DispatcherTimer>? timers = null;
+            long timersVersion = 0;
+
+            lock (InstanceLock)
+            {
+                if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
+                {
+                    if (_dueTimeFound && _dueTimeInMs - currentTimeInTicks <= 0)
+                    {
+                        timers = _timers;
+                        timersVersion = _timersVersion;
+                    }
+                }
+            }
+
+            if (timers != null)
+            {
+                DispatcherTimer? timer = null;
+                int iTimer = 0;
+
+                do
+                {
+                    lock (InstanceLock)
+                    {
+                        timer = null;
+
+                        // If the timers collection changed while we are in the middle of
+                        // looking for timers, start over.
+                        if (timersVersion != _timersVersion)
+                        {
+                            timersVersion = _timersVersion;
+                            iTimer = 0;
+                        }
+
+                        while (iTimer < _timers.Count)
+                        {
+                            // WARNING: this is vulnerable to wrapping
+                            if (timers[iTimer].DueTimeInMs - currentTimeInTicks <= 0)
+                            {
+                                // Remove this timer from our list.
+                                // Do not increment the index.
+                                timer = timers[iTimer];
+                                timers.RemoveAt(iTimer);
+                                break;
+                            }
+                            else
+                            {
+                                iTimer++;
+                            }
+                        }
+                    }
+
+                    // Now that we are outside of the lock, promote the timer.
+                    if (timer != null)
+                    {
+                        timer.Promote();
+                    }
+                } while (timer != null);
+            }
+        }
+        finally
+        {
+            RescheduleTimers();
+        }
+    }
+
+    internal static List<DispatcherTimer> SnapshotTimersForUnitTests() =>
+        s_uiThread!._timers.ToList();
+}

+ 98 - 136
src/Avalonia.Base/Threading/Dispatcher.cs

@@ -1,159 +1,121 @@
 using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
 using System.Threading;
-using System.Threading.Tasks;
 using Avalonia.Platform;
 
-namespace Avalonia.Threading
+namespace Avalonia.Threading;
+
+/// <summary>
+/// Provides services for managing work items on a thread.
+/// </summary>
+/// <remarks>
+/// In Avalonia, there is usually only a single <see cref="Dispatcher"/> in the application -
+/// the one for the UI thread, retrieved via the <see cref="UIThread"/> property.
+/// </remarks>
+public partial class Dispatcher : IDispatcher
 {
-    /// <summary>
-    /// Provides services for managing work items on a thread.
-    /// </summary>
-    /// <remarks>
-    /// In Avalonia, there is usually only a single <see cref="Dispatcher"/> in the application -
-    /// the one for the UI thread, retrieved via the <see cref="UIThread"/> property.
-    /// </remarks>
-    public class Dispatcher : IDispatcher
+    private IDispatcherImpl _impl;
+    internal object InstanceLock { get; } = new();
+    private bool _hasShutdownFinished;
+    private IControlledDispatcherImpl? _controlledImpl;
+    private static Dispatcher? s_uiThread;
+    private IDispatcherImplWithPendingInput? _pendingInputImpl;
+    private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl;
+
+    internal Dispatcher(IDispatcherImpl impl)
     {
-        private readonly JobRunner _jobRunner;
-        private IPlatformThreadingInterface? _platform;
-
-        public static Dispatcher UIThread { get; } =
-            new Dispatcher(AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>());
-
-        public Dispatcher(IPlatformThreadingInterface? platform)
-        {
-            _platform = platform;
-            _jobRunner = new JobRunner(platform);
-
-            if (_platform != null)
-            {
-                _platform.Signaled += _jobRunner.RunJobs;
-            }
-        }
-
-        /// <summary>
-        /// Checks that the current thread is the UI thread.
-        /// </summary>
-        public bool CheckAccess() => _platform?.CurrentThreadIsLoopThread ?? true;
-
-        /// <summary>
-        /// Checks that the current thread is the UI thread and throws if not.
-        /// </summary>
-        /// <exception cref="InvalidOperationException">
-        /// The current thread is not the UI thread.
-        /// </exception>
-        public void VerifyAccess()
-        {
-            if (!CheckAccess())
-                throw new InvalidOperationException("Call from invalid thread");
-        }
-
-        /// <summary>
-        /// Runs the dispatcher's main loop.
-        /// </summary>
-        /// <param name="cancellationToken">
-        /// A cancellation token used to exit the main loop.
-        /// </param>
-        public void MainLoop(CancellationToken cancellationToken)
-        {
-            var platform = AvaloniaLocator.Current.GetRequiredService<IPlatformThreadingInterface>();
-            cancellationToken.Register(() => platform.Signal(DispatcherPriority.Send));
-            platform.RunLoop(cancellationToken);
-        }
-
-        /// <summary>
-        /// Runs continuations pushed on the loop.
-        /// </summary>
-        public void RunJobs()
-        {
-            _jobRunner.RunJobs(null);
-        }
-
-        /// <summary>
-        /// Use this method to ensure that more prioritized tasks are executed
-        /// </summary>
-        /// <param name="minimumPriority"></param>
-        public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority);
-        
-        /// <summary>
-        /// Use this method to check if there are more prioritized tasks
-        /// </summary>
-        /// <param name="minimumPriority"></param>
-        public bool HasJobsWithPriority(DispatcherPriority minimumPriority) =>
-            _jobRunner.HasJobsWithPriority(minimumPriority);
-
-        /// <inheritdoc/>
-        public Task InvokeAsync(Action action, DispatcherPriority priority = default)
-        {
-            _ = action ?? throw new ArgumentNullException(nameof(action));
-            return _jobRunner.InvokeAsync(action, priority);
-        }
-
-        /// <inheritdoc/>
-        public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority = default)
-        {
-            _ = function ?? throw new ArgumentNullException(nameof(function));
-            return _jobRunner.InvokeAsync(function, priority);
-        }
-
-        /// <inheritdoc/>
-        public Task InvokeAsync(Func<Task> function, DispatcherPriority priority = default)
-        {
-            _ = function ?? throw new ArgumentNullException(nameof(function));
-            return _jobRunner.InvokeAsync(function, priority).Unwrap();
-        }
+        _impl = impl;
+        impl.Timer += OnOSTimer;
+        impl.Signaled += Signaled;
+        _controlledImpl = _impl as IControlledDispatcherImpl;
+        _pendingInputImpl = _impl as IDispatcherImplWithPendingInput;
+        _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing;
+        if (_backgroundProcessingImpl != null)
+            _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing;
+    }
+    
+    public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher();
 
-        /// <inheritdoc/>
-        public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority = default)
+    private static Dispatcher CreateUIThreadDispatcher()
+    {
+        var impl = AvaloniaLocator.Current.GetService<IDispatcherImpl>();
+        if (impl == null)
         {
-            _ = function ?? throw new ArgumentNullException(nameof(function));
-            return _jobRunner.InvokeAsync(function, priority).Unwrap();
+            var platformThreading = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>();
+            if (platformThreading != null)
+                impl = new LegacyDispatcherImpl(platformThreading);
+            else
+                impl = new NullDispatcherImpl();
         }
+        return new Dispatcher(impl);
+    }
 
-        /// <inheritdoc/>
-        public void Post(Action action, DispatcherPriority priority = default)
-        {
-            _ = action ?? throw new ArgumentNullException(nameof(action));
-            _jobRunner.Post(action, priority);
-        }
+    /// <summary>
+    /// Checks that the current thread is the UI thread.
+    /// </summary>
+    public bool CheckAccess() => _impl?.CurrentThreadIsLoopThread ?? true;
 
-        /// <inheritdoc/>
-        public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default)
+    /// <summary>
+    /// Checks that the current thread is the UI thread and throws if not.
+    /// </summary>
+    /// <exception cref="InvalidOperationException">
+    /// The current thread is not the UI thread.
+    /// </exception>
+    public void VerifyAccess()
+    {
+        if (!CheckAccess())
         {
-            _ = action ?? throw new ArgumentNullException(nameof(action));
-            _jobRunner.Post(action, arg, priority);
-        }
+            // Used to inline VerifyAccess.
+            [DoesNotReturn]
+            [MethodImpl(MethodImplOptions.NoInlining)]
+            static void ThrowVerifyAccess()
+                => throw new InvalidOperationException("Call from invalid thread");
 
-        /// <summary>
-        /// This is needed for platform backends that don't have internal priority system (e. g. win32)
-        /// To ensure that there are no jobs with higher priority
-        /// </summary>
-        /// <param name="currentPriority"></param>
-        internal void EnsurePriority(DispatcherPriority currentPriority)
-        {
-            if (currentPriority == DispatcherPriority.MaxValue)
-                return;
-            currentPriority += 1;
-            _jobRunner.RunJobs(currentPriority);
+            ThrowVerifyAccess();
         }
+    }
 
-        /// <summary>
-        /// Allows unit tests to change the platform threading interface.
-        /// </summary>
-        internal void UpdateServices()
+    internal void Shutdown()
+    {
+        DispatcherOperation? operation = null;
+        _impl.Timer -= PromoteTimers;
+        _impl.Signaled -= Signaled;
+        do
         {
-            if (_platform != null)
+            lock(InstanceLock)
             {
-                _platform.Signaled -= _jobRunner.RunJobs;
+                if(_queue.MaxPriority != DispatcherPriority.Invalid)
+                {
+                    operation = _queue.Peek();
+                }
+                else
+                {
+                    operation = null;
+                }
             }
 
-            _platform = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>();
-            _jobRunner.UpdateServices();
-
-            if (_platform != null)
+            if(operation != null)
             {
-                _platform.Signaled += _jobRunner.RunJobs;
+                operation.Abort();
             }
-        }
+        } while(operation != null);
+        _impl.UpdateTimer(null);
+        _hasShutdownFinished = true;
+    }
+
+    /// <summary>
+    /// Runs the dispatcher's main loop.
+    /// </summary>
+    /// <param name="cancellationToken">
+    /// A cancellation token used to exit the main loop.
+    /// </param>
+    public void MainLoop(CancellationToken cancellationToken)
+    {
+        if (_controlledImpl == null)
+            throw new PlatformNotSupportedException();
+        cancellationToken.Register(() => RequestProcessing());
+        _controlledImpl.RunLoop(cancellationToken);
     }
 }

+ 308 - 0
src/Avalonia.Base/Threading/DispatcherOperation.cs

@@ -0,0 +1,308 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Avalonia.Threading;
+
+public class DispatcherOperation
+{
+    protected readonly bool ThrowOnUiThread;
+    public DispatcherOperationStatus Status { get; protected set; }
+    public Dispatcher Dispatcher { get; }
+
+    public DispatcherPriority Priority
+    {
+        get => _priority;
+        set
+        {
+            _priority = value;
+            // Dispatcher is null in ctor
+            // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
+            Dispatcher?.SetPriority(this, value);
+        }
+    }
+
+    protected object? Callback;
+    protected object? TaskSource;
+    
+    internal DispatcherOperation? SequentialPrev { get; set; }
+    internal DispatcherOperation? SequentialNext { get; set; }
+    internal DispatcherOperation? PriorityPrev { get; set; }
+    internal DispatcherOperation? PriorityNext { get; set; }
+    internal PriorityChain? Chain { get; set; }
+    
+    internal bool IsQueued => Chain != null;
+
+    private EventHandler? _aborted;
+    private EventHandler? _completed;
+    private DispatcherPriority _priority;
+
+    internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread) :
+        this(dispatcher, priority, throwOnUiThread)
+    {
+        Callback = callback;
+    }
+
+    private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, bool throwOnUiThread)
+    {
+        ThrowOnUiThread = throwOnUiThread;
+        Priority = priority;
+        Dispatcher = dispatcher;
+    }
+
+    /// <summary>
+    ///     An event that is raised when the operation is aborted or canceled.
+    /// </summary>
+    public event EventHandler Aborted
+    {
+        add
+        {
+            lock (Dispatcher.InstanceLock)
+            {
+                _aborted += value;
+            }
+        }
+
+        remove
+        {
+            lock(Dispatcher.InstanceLock)
+            {
+                _aborted -= value;
+            }
+        }
+    }
+
+    /// <summary>
+    ///     An event that is raised when the operation completes.
+    /// </summary>
+    /// <remarks>
+    ///     Completed indicates that the operation was invoked and has
+    ///     either completed successfully or faulted. Note that a canceled
+    ///     or aborted operation is never is never considered completed.
+    /// </remarks>
+    public event EventHandler Completed
+    {
+        add
+        {
+            lock (Dispatcher.InstanceLock)
+            {
+                _completed += value;
+            }
+        }
+        
+        remove
+        {
+            lock(Dispatcher.InstanceLock)
+            {
+                _completed -= value;
+            }
+        }
+    }
+    
+    public void Abort()
+    {
+        lock (Dispatcher.InstanceLock)
+        {
+            if (Status == DispatcherOperationStatus.Pending)
+                return;
+            Dispatcher.Abort(this);
+        }
+    }
+
+    public void Wait()
+    {
+        if (Dispatcher.CheckAccess())
+            throw new InvalidOperationException("Wait is only supported on background thread");
+        GetTask().Wait();
+    }
+
+    public Task GetTask() => GetTaskCore();
+    
+    /// <summary>
+    ///     Returns an awaiter for awaiting the completion of the operation.
+    /// </summary>
+    /// <remarks>
+    ///     This method is intended to be used by compilers.
+    /// </remarks>
+    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+    public TaskAwaiter GetAwaiter()
+    {
+        return GetTask().GetAwaiter();
+    }
+
+    internal void DoAbort()
+    {
+        Status = DispatcherOperationStatus.Aborted;
+        AbortTask();
+        _aborted?.Invoke(this, EventArgs.Empty);
+    }
+    
+    internal void Execute()
+    {
+        lock (Dispatcher.InstanceLock)
+        {
+            Status = DispatcherOperationStatus.Executing;
+        }
+
+        try
+        {
+            InvokeCore();
+        }
+        finally
+        {
+            _completed?.Invoke(this, EventArgs.Empty);
+        }
+    }
+    
+    protected virtual void InvokeCore()
+    {
+        try
+        {
+            ((Action)Callback!)();
+            lock (Dispatcher.InstanceLock)
+            {
+                Status = DispatcherOperationStatus.Completed;
+                if (TaskSource is TaskCompletionSource<object?> tcs)
+                    tcs.SetResult(null);
+            }
+        }
+        catch (Exception e)
+        {
+            lock (Dispatcher.InstanceLock)
+            {
+                Status = DispatcherOperationStatus.Completed;
+                if (TaskSource is TaskCompletionSource<object?> tcs)
+                    tcs.SetException(e);
+            }
+
+            if (ThrowOnUiThread)
+                throw;
+        }
+    }
+
+    internal virtual object? GetResult() => null;
+    
+    protected virtual void AbortTask() => (TaskSource as TaskCompletionSource<object?>)?.SetCanceled();
+
+    private static CancellationToken CreateCancelledToken()
+    {
+        var cts = new CancellationTokenSource();
+        cts.Cancel();
+        return cts.Token;
+    }
+
+    private static readonly Task s_abortedTask = Task.FromCanceled(CreateCancelledToken());
+
+    protected virtual Task GetTaskCore()
+    {
+        lock (Dispatcher.InstanceLock)
+        {
+            if (Status == DispatcherOperationStatus.Aborted)
+                return s_abortedTask;
+            if (Status == DispatcherOperationStatus.Completed)
+                return Task.CompletedTask;
+            if (TaskSource is not TaskCompletionSource<object?> tcs)
+                TaskSource = tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+            return tcs.Task;
+        }
+    }
+}
+
+public class DispatcherOperation<T> : DispatcherOperation
+{
+    public DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Func<T> callback) : base(dispatcher, priority, false)
+    {
+        TaskSource = new TaskCompletionSource<T>();
+        Callback = callback;
+    }
+
+    private TaskCompletionSource<T> TaskCompletionSource => (TaskCompletionSource<T>)TaskSource!;
+
+    public new Task<T> GetTask() => TaskCompletionSource!.Task;
+
+    protected override Task GetTaskCore() => GetTask();
+
+    protected override void AbortTask() => TaskCompletionSource.SetCanceled();
+
+    internal override object? GetResult() => GetTask().Result;
+
+    protected override void InvokeCore()
+    {
+        try
+        {
+            var result = ((Func<T>)Callback!)();
+            lock (Dispatcher.InstanceLock)
+            {
+                Status = DispatcherOperationStatus.Completed;
+                TaskCompletionSource.SetResult(result);
+            }
+        }
+        catch (Exception e)
+        {
+            lock (Dispatcher.InstanceLock)
+            {
+                Status = DispatcherOperationStatus.Completed;
+                TaskCompletionSource.SetException(e);
+            }
+        }
+    }
+
+    public T Result
+    {
+        get
+        {
+            if (TaskCompletionSource.Task.IsCompleted || !Dispatcher.CheckAccess())
+                return TaskCompletionSource.Task.GetAwaiter().GetResult();
+            throw new InvalidOperationException("Synchronous wait is only supported on non-UI threads");
+        }
+    }
+}
+
+internal class SendOrPostCallbackDispatcherOperation : DispatcherOperation
+{
+    private readonly object? _arg;
+
+    internal SendOrPostCallbackDispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, 
+        SendOrPostCallback callback, object? arg, bool throwOnUiThread) 
+        : base(dispatcher, priority, throwOnUiThread)
+    {
+        Callback = callback;
+        _arg = arg;
+    }
+    
+    protected override void InvokeCore()
+    {
+        try
+        {
+            ((SendOrPostCallback)Callback!)(_arg);
+            lock (Dispatcher.InstanceLock)
+            {
+                Status = DispatcherOperationStatus.Completed;
+                if (TaskSource is TaskCompletionSource<object?> tcs)
+                    tcs.SetResult(null);
+            }
+        }
+        catch (Exception e)
+        {
+            lock (Dispatcher.InstanceLock)
+            {
+                Status = DispatcherOperationStatus.Completed;
+                if (TaskSource is TaskCompletionSource<object?> tcs)
+                    tcs.SetException(e);
+            }
+
+            if (ThrowOnUiThread)
+                throw;
+        }
+    }
+}
+
+public enum DispatcherOperationStatus
+{
+    Pending = 0,
+    Aborted = 1,
+    Completed = 2,
+    Executing = 3,
+}

+ 31 - 7
src/Avalonia.Base/Threading/DispatcherPriority.cs

@@ -16,31 +16,49 @@ namespace Avalonia.Threading
         {
             Value = value;
         }
-
+        
         /// <summary>
-        /// Minimum possible priority
+        /// Minimum possible priority that's actually dispatched, default value
         /// </summary>
-        public static readonly DispatcherPriority MinValue = new(0);
+        internal static readonly DispatcherPriority MinimumActiveValue = new(0);
 
+        /// <summary>
+        /// A dispatcher priority for jobs that shouldn't be executed yet
+        /// </summary>
+        public static DispatcherPriority Inactive => new(MinimumActiveValue - 1);
+        /// <summary>
+        /// Minimum valid priority
+        /// </summary>
+        internal static readonly DispatcherPriority MinValue = new(Inactive);
+        
+        /// <summary>
+        /// Used internally in dispatcher code
+        /// </summary>
+        public static DispatcherPriority Invalid => new(MinimumActiveValue - 2);
+        
+        
+        
         /// <summary>
         /// The job will be processed when the system is idle.
         /// </summary>
-        [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue;
+        [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinimumActiveValue;
 
         /// <summary>
         /// The job will be processed when the application is idle.
         /// </summary>
-        [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue;
+        [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = new (SystemIdle + 1);
 
         /// <summary>
         /// The job will be processed after background operations have completed.
         /// </summary>
-        [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue;
+        [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = new(ApplicationIdle + 1);
 
         /// <summary>
         /// The job will be processed with normal priority.
         /// </summary>
-        public static readonly DispatcherPriority Normal = MinValue;
+#pragma warning disable CS0618
+        public static readonly DispatcherPriority Normal = new(ContextIdle + 1);
+#pragma warning restore CS0618
 
         /// <summary>
         /// The job will be processed after other non-idle operations have completed.
@@ -127,5 +145,11 @@ namespace Avalonia.Threading
 
         /// <inheritdoc />
         public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value);
+
+        public static void Validate(DispatcherPriority priority, string parameterName)
+        {
+            if (priority < Inactive || priority > MaxValue)
+                throw new ArgumentException("Invalid DispatcherPriority value", parameterName);
+        }
     }
 }

+ 418 - 0
src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs

@@ -0,0 +1,418 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Avalonia.Threading;
+
+namespace Avalonia.Threading;
+
+
+internal class DispatcherPriorityQueue
+{
+    // Priority chains...
+    private readonly SortedList<int, PriorityChain> _priorityChains; // NOTE: should be Priority
+    private readonly Stack<PriorityChain> _cacheReusableChains;
+
+    // Sequential chain...
+    private DispatcherOperation? _head;
+    private DispatcherOperation? _tail;
+    
+    public DispatcherPriorityQueue()
+    {
+        // Build the collection of priority chains.
+        _priorityChains = new SortedList<int, PriorityChain>(); // NOTE: should be Priority
+        _cacheReusableChains = new Stack<PriorityChain>(10);
+
+        _head = _tail = null;
+    }
+
+    // NOTE: not used
+    // public int Count {get{return _count;}}
+
+    public DispatcherPriority MaxPriority // NOTE: should be Priority
+    {
+        get
+        {
+            int count = _priorityChains.Count;
+
+            if (count > 0)
+            {
+                return _priorityChains.Keys[count - 1];
+            }
+            else
+            {
+                return DispatcherPriority.Invalid; // NOTE: should be Priority.Invalid;
+            }
+        }
+    }
+
+    public DispatcherOperation Enqueue(DispatcherPriority priority, DispatcherOperation item) // NOTE: should be Priority
+    {
+        // Find the existing chain for this priority, or create a new one
+        // if one does not exist.
+        PriorityChain chain = GetChain(priority);
+
+        // Step 1: Append this to the end of the "sequential" linked list.
+        InsertItemInSequentialChain(item, _tail);
+
+        // Step 2: Append the item into the priority chain.
+        InsertItemInPriorityChain(item, chain, chain.Tail);
+
+        return item;
+    }
+
+    public DispatcherOperation Dequeue()
+    {
+        // Get the max-priority chain.
+        int count = _priorityChains.Count;
+        if (count > 0)
+        {
+            PriorityChain chain = _priorityChains.Values[count - 1];
+            Debug.Assert(chain != null, "PriorityQueue.Dequeue: a chain should exist.");
+
+            DispatcherOperation? item = chain.Head;
+            Debug.Assert(item != null, "PriorityQueue.Dequeue: a priority item should exist.");
+
+            RemoveItem(item);
+
+            return item;
+        }
+        else
+        {
+            throw new InvalidOperationException();
+        }
+    }
+
+    public DispatcherOperation? Peek()
+    {
+        // Get the max-priority chain.
+        int count = _priorityChains.Count;
+        if (count > 0)
+        {
+            PriorityChain chain = _priorityChains.Values[count - 1];
+            Debug.Assert(chain != null, "PriorityQueue.Peek: a chain should exist.");
+
+            DispatcherOperation? item = chain.Head;
+            Debug.Assert(item != null, "PriorityQueue.Peek: a priority item should exist.");
+
+            return item;
+        }
+
+        return null;
+    }
+    
+    public void RemoveItem(DispatcherOperation item)
+    {
+        Debug.Assert(item != null, "PriorityQueue.RemoveItem: invalid item.");
+        Debug.Assert(item.Chain != null, "PriorityQueue.RemoveItem: a chain should exist.");
+        
+        // Step 1: Remove the item from its priority chain.
+        RemoveItemFromPriorityChain(item);
+
+        // Step 2: Remove the item from the sequential chain.
+        RemoveItemFromSequentialChain(item);
+    }
+
+    public void ChangeItemPriority(DispatcherOperation item, DispatcherPriority priority) // NOTE: should be Priority
+    {
+        // Remove the item from its current priority and insert it into
+        // the new priority chain.  Note that this does not change the
+        // sequential ordering.
+
+        // Step 1: Remove the item from the priority chain.
+        RemoveItemFromPriorityChain(item);
+
+        // Step 2: Insert the item into the new priority chain.
+        // Find the existing chain for this priority, or create a new one
+        // if one does not exist.
+        PriorityChain chain = GetChain(priority);
+        InsertItemInPriorityChain(item, chain);
+    }
+
+    private PriorityChain GetChain(DispatcherPriority priority) // NOTE: should be Priority
+    {
+        PriorityChain? chain = null;
+
+        int count = _priorityChains.Count;
+        if (count > 0)
+        {
+            if (priority == _priorityChains.Keys[0])
+            {
+                chain = _priorityChains.Values[0];
+            }
+            else if (priority == _priorityChains.Keys[count - 1])
+            {
+                chain = _priorityChains.Values[count - 1];
+            }
+            else if ((priority > _priorityChains.Keys[0]) &&
+                     (priority < _priorityChains.Keys[count - 1]))
+            {
+                _priorityChains.TryGetValue(priority, out chain);
+            }
+        }
+
+        if (chain == null)
+        {
+            if (_cacheReusableChains.Count > 0)
+            {
+                chain = _cacheReusableChains.Pop();
+                chain.Priority = priority;
+            }
+            else
+            {
+                chain = new PriorityChain(priority);
+            }
+
+            _priorityChains.Add(priority, chain);
+        }
+
+        return chain;
+    }
+
+    private void InsertItemInPriorityChain(DispatcherOperation item, PriorityChain chain)
+    {
+        // Scan along the sequential chain, in the previous direction,
+        // looking for an item that is already in the new chain.  We will
+        // insert ourselves after the item we found.  We can short-circuit
+        // this search if the new chain is empty.
+        if (chain.Head == null)
+        {
+            Debug.Assert(chain.Tail == null,
+                "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should be null.");
+            InsertItemInPriorityChain(item, chain, null);
+        }
+        else
+        {
+            Debug.Assert(chain.Tail != null,
+                "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should not be null.");
+
+            DispatcherOperation? after;
+
+            // Search backwards along the sequential chain looking for an
+            // item already in this list.
+            for (after = item.SequentialPrev; after != null; after = after.SequentialPrev)
+            {
+                if (after.Chain == chain)
+                {
+                    break;
+                }
+            }
+
+            InsertItemInPriorityChain(item, chain, after);
+        }
+    }
+
+    internal void InsertItemInPriorityChain(DispatcherOperation item, PriorityChain chain, DispatcherOperation? after)
+    {
+        Debug.Assert(chain != null, "PriorityQueue.InsertItemInPriorityChain: a chain must be provided.");
+        Debug.Assert(item.Chain == null && item.PriorityPrev == null && item.PriorityNext == null,
+            "PriorityQueue.InsertItemInPriorityChain: item must not already be in a priority chain.");
+
+        item.Chain = chain;
+
+        if (after == null)
+        {
+            // Note: passing null for after means insert at the head.
+
+            if (chain.Head != null)
+            {
+                Debug.Assert(chain.Tail != null,
+                    "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should not be null.");
+
+                chain.Head.PriorityPrev = item;
+                item.PriorityNext = chain.Head;
+                chain.Head = item;
+            }
+            else
+            {
+                Debug.Assert(chain.Tail == null,
+                    "PriorityQueue.InsertItemInPriorityChain: both the head and the tail should be null.");
+
+                chain.Head = chain.Tail = item;
+            }
+        }
+        else
+        {
+            item.PriorityPrev = after;
+
+            if (after.PriorityNext != null)
+            {
+                item.PriorityNext = after.PriorityNext;
+                after.PriorityNext.PriorityPrev = item;
+                after.PriorityNext = item;
+            }
+            else
+            {
+                Debug.Assert(item.Chain.Tail == after,
+                    "PriorityQueue.InsertItemInPriorityChain: the chain's tail should be the item we are inserting after.");
+                after.PriorityNext = item;
+                chain.Tail = item;
+            }
+        }
+
+        chain.Count++;
+    }
+
+    private void RemoveItemFromPriorityChain(DispatcherOperation item)
+    {
+        Debug.Assert(item != null, "PriorityQueue.RemoveItemFromPriorityChain: invalid item.");
+        Debug.Assert(item.Chain != null, "PriorityQueue.RemoveItemFromPriorityChain: a chain should exist.");
+
+        // Step 1: Fix up the previous link
+        if (item.PriorityPrev != null)
+        {
+            Debug.Assert(item.Chain.Head != item,
+                "PriorityQueue.RemoveItemFromPriorityChain: the head should not point to this item.");
+
+            item.PriorityPrev.PriorityNext = item.PriorityNext;
+        }
+        else
+        {
+            Debug.Assert(item.Chain.Head == item,
+                "PriorityQueue.RemoveItemFromPriorityChain: the head should point to this item.");
+
+            item.Chain.Head = item.PriorityNext;
+        }
+
+        // Step 2: Fix up the next link
+        if (item.PriorityNext != null)
+        {
+            Debug.Assert(item.Chain.Tail != item,
+                "PriorityQueue.RemoveItemFromPriorityChain: the tail should not point to this item.");
+
+            item.PriorityNext.PriorityPrev = item.PriorityPrev;
+        }
+        else
+        {
+            Debug.Assert(item.Chain.Tail == item,
+                "PriorityQueue.RemoveItemFromPriorityChain: the tail should point to this item.");
+
+            item.Chain.Tail = item.PriorityPrev;
+        }
+
+        // Step 3: cleanup
+        item.PriorityPrev = item.PriorityNext = null;
+        item.Chain.Count--;
+        if (item.Chain.Count == 0)
+        {
+            if (item.Chain.Priority == _priorityChains.Keys[_priorityChains.Count - 1])
+            {
+                _priorityChains.RemoveAt(_priorityChains.Count - 1);
+            }
+            else
+            {
+                _priorityChains.Remove(item.Chain.Priority);
+            }
+
+            if (_cacheReusableChains.Count < 10)
+            {
+                item.Chain.Priority = DispatcherPriority.Invalid;
+                _cacheReusableChains.Push(item.Chain);
+            }
+        }
+
+        item.Chain = null;
+    }
+
+    internal void InsertItemInSequentialChain(DispatcherOperation item, DispatcherOperation? after)
+    {
+        Debug.Assert(item.SequentialPrev == null && item.SequentialNext == null,
+            "PriorityQueue.InsertItemInSequentialChain: item must not already be in the sequential chain.");
+
+        if (after == null)
+        {
+            // Note: passing null for after means insert at the head.
+
+            if (_head != null)
+            {
+                Debug.Assert(_tail != null,
+                    "PriorityQueue.InsertItemInSequentialChain: both the head and the tail should not be null.");
+
+                _head.SequentialPrev = item;
+                item.SequentialNext = _head;
+                _head = item;
+            }
+            else
+            {
+                Debug.Assert(_tail == null,
+                    "PriorityQueue.InsertItemInSequentialChain: both the head and the tail should be null.");
+
+                _head = _tail = item;
+            }
+        }
+        else
+        {
+            item.SequentialPrev = after;
+
+            if (after.SequentialNext != null)
+            {
+                item.SequentialNext = after.SequentialNext;
+                after.SequentialNext.SequentialPrev = item;
+                after.SequentialNext = item;
+            }
+            else
+            {
+                Debug.Assert(_tail == after,
+                    "PriorityQueue.InsertItemInSequentialChain: the tail should be the item we are inserting after.");
+                after.SequentialNext = item;
+                _tail = item;
+            }
+        }
+    }
+
+    private void RemoveItemFromSequentialChain(DispatcherOperation item)
+    {
+        Debug.Assert(item != null, "PriorityQueue.RemoveItemFromSequentialChain: invalid item.");
+
+        // Step 1: Fix up the previous link
+        if (item.SequentialPrev != null)
+        {
+            Debug.Assert(_head != item,
+                "PriorityQueue.RemoveItemFromSequentialChain: the head should not point to this item.");
+
+            item.SequentialPrev.SequentialNext = item.SequentialNext;
+        }
+        else
+        {
+            Debug.Assert(_head == item,
+                "PriorityQueue.RemoveItemFromSequentialChain: the head should point to this item.");
+
+            _head = item.SequentialNext;
+        }
+
+        // Step 2: Fix up the next link
+        if (item.SequentialNext != null)
+        {
+            Debug.Assert(_tail != item,
+                "PriorityQueue.RemoveItemFromSequentialChain: the tail should not point to this item.");
+
+            item.SequentialNext.SequentialPrev = item.SequentialPrev;
+        }
+        else
+        {
+            Debug.Assert(_tail == item,
+                "PriorityQueue.RemoveItemFromSequentialChain: the tail should point to this item.");
+
+            _tail = item.SequentialPrev;
+        }
+
+        // Step 3: cleanup
+        item.SequentialPrev = item.SequentialNext = null;
+    }
+}
+
+
+internal class PriorityChain
+{
+    public PriorityChain(DispatcherPriority priority) // NOTE: should be Priority
+    {
+        Priority = priority;
+    }
+
+    public DispatcherPriority Priority { get; set; } // NOTE: should be Priority
+
+    public int Count { get; set; }
+
+    public DispatcherOperation? Head { get; set; }
+
+    public DispatcherOperation? Tail { get; set; }
+}

+ 284 - 139
src/Avalonia.Base/Threading/DispatcherTimer.cs

@@ -1,207 +1,352 @@
 using System;
 using Avalonia.Reactive;
-using Avalonia.Platform;
 
-namespace Avalonia.Threading
+namespace Avalonia.Threading;
+
+/// <summary>
+///     A timer that is integrated into the Dispatcher queues, and will
+///     be processed after a given amount of time at a specified priority.
+/// </summary>
+public partial class DispatcherTimer
 {
     /// <summary>
-    /// A timer that uses a <see cref="Dispatcher"/> to fire at a specified interval.
+    ///     Creates a timer that uses theUI thread's Dispatcher2 to
+    ///     process the timer event at background priority.
     /// </summary>
-    public class DispatcherTimer
+    public DispatcherTimer() : this(DispatcherPriority.Background)
     {
-        private IDisposable? _timer;
+    }
 
-        private readonly DispatcherPriority _priority;
+    /// <summary>
+    ///     Creates a timer that uses the UI thread's Dispatcher2 to
+    ///     process the timer event at the specified priority.
+    /// </summary>
+    /// <param name="priority">
+    ///     The priority to process the timer at.
+    /// </param>
+    public DispatcherTimer(DispatcherPriority priority) : this(Threading.Dispatcher.UIThread, priority,
+        TimeSpan.FromMilliseconds(0))
+    {
+    }
 
-        private TimeSpan _interval;
+    /// <summary>
+    ///     Creates a timer that uses the specified Dispatcher2 to
+    ///     process the timer event at the specified priority.
+    /// </summary>
+    /// <param name="priority">
+    ///     The priority to process the timer at.
+    /// </param>
+    /// <param name="dispatcher">
+    ///     The dispatcher to use to process the timer.
+    /// </param>
+    internal DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher) : this(dispatcher, priority,
+        TimeSpan.FromMilliseconds(0))
+    {
+    }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
-        /// </summary>
-        public DispatcherTimer() : this(DispatcherPriority.Background)
+    /// <summary>
+    ///     Creates a timer that uses the UI thread's Dispatcher2 to
+    ///     process the timer event at the specified priority after the specified timeout.
+    /// </summary>
+    /// <param name="interval">
+    ///     The interval to tick the timer after.
+    /// </param>
+    /// <param name="priority">
+    ///     The priority to process the timer at.
+    /// </param>
+    /// <param name="callback">
+    ///     The callback to call when the timer ticks.
+    /// </param>
+    public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback)
+        : this(Threading.Dispatcher.UIThread, priority, interval)
+    {
+        if (callback == null)
         {
+            throw new ArgumentNullException("callback");
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
-        /// </summary>
-        /// <param name="priority">The priority to use.</param>
-        public DispatcherTimer(DispatcherPriority priority)
-        {
-            _priority = priority;
-        }
+        Tick += callback;
+        Start();
+    }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
-        /// </summary>
-        /// <param name="interval">The interval at which to tick.</param>
-        /// <param name="priority">The priority to use.</param>
-        /// <param name="callback">The event to call when the timer ticks.</param>
-        public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) : this(priority)
-        {
-            _priority = priority;
-            Interval = interval;
-            Tick += callback;
-        }
+    /// <summary>
+    ///     Gets the dispatcher this timer is associated with.
+    /// </summary>
+    public Dispatcher Dispatcher
+    {
+        get { return _dispatcher; }
+    }
 
-        /// <summary>
-        /// Finalizes an instance of the <see cref="DispatcherTimer"/> class.
-        /// </summary>
-        ~DispatcherTimer()
+    /// <summary>
+    ///     Gets or sets whether the timer is running.
+    /// </summary>
+    public bool IsEnabled
+    {
+        get { return _isEnabled; }
+
+        set
         {
-            if (_timer != null)
+            lock (_instanceLock)
             {
-                Stop();
+                if (!value && _isEnabled)
+                {
+                    Stop();
+                }
+                else if (value && !_isEnabled)
+                {
+                    Start();
+                }
             }
         }
+    }
 
-        /// <summary>
-        /// Raised when the timer ticks.
-        /// </summary>
-        public event EventHandler? Tick;
+    /// <summary>
+    ///     Gets or sets the time between timer ticks.
+    /// </summary>
+    public TimeSpan Interval
+    {
+        get { return _interval; }
 
-        /// <summary>
-        /// Gets or sets the interval at which the timer ticks.
-        /// </summary>
-        public TimeSpan Interval
+        set
         {
-            get
+            bool updateOSTimer = false;
+
+            if (value.TotalMilliseconds < 0)
+                throw new ArgumentOutOfRangeException("value",
+                    "TimeSpan period must be greater than or equal to zero.");
+
+            if (value.TotalMilliseconds > Int32.MaxValue)
+                throw new ArgumentOutOfRangeException("value",
+                    "TimeSpan period must be less than or equal to Int32.MaxValue.");
+
+            lock (_instanceLock)
             {
-                return _interval;
+                _interval = value;
+
+                if (_isEnabled)
+                {
+                    DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds;
+                    updateOSTimer = true;
+                }
             }
 
-            set
+            if (updateOSTimer)
             {
-                bool enabled = IsEnabled;
-                Stop();
-                _interval = value;
-                IsEnabled = enabled;
+                _dispatcher.RescheduleTimers();
             }
         }
+    }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether the timer is running.
-        /// </summary>
-        public bool IsEnabled
+    /// <summary>
+    ///     Starts the timer.
+    /// </summary>
+    public void Start()
+    {
+        lock (_instanceLock)
         {
-            get
+            if (!_isEnabled)
             {
-                return _timer != null;
+                _isEnabled = true;
+
+                Restart();
             }
+        }
+    }
 
-            set
+    /// <summary>
+    ///     Stops the timer.
+    /// </summary>
+    public void Stop()
+    {
+        bool updateOSTimer = false;
+
+        lock (_instanceLock)
+        {
+            if (_isEnabled)
             {
-                if (IsEnabled != value)
+                _isEnabled = false;
+                updateOSTimer = true;
+
+                // If the operation is in the queue, abort it.
+                if (_operation != null)
                 {
-                    if (value)
-                    {
-                        Start();
-                    }
-                    else
-                    {
-                        Stop();
-                    }
+                    _operation.Abort();
+                    _operation = null;
                 }
             }
         }
 
-        /// <summary>
-        /// Gets or sets user-defined data associated with the timer.
-        /// </summary>
-        public object? Tag
+        if (updateOSTimer)
         {
-            get;
-            set;
+            _dispatcher.RemoveTimer(this);
         }
+    }
+    
+    /// <summary>
+    /// Starts a new timer.
+    /// </summary>
+    /// <param name="action">
+    /// The method to call on timer tick. If the method returns false, the timer will stop.
+    /// </param>
+    /// <param name="interval">The interval at which to tick.</param>
+    /// <param name="priority">The priority to use.</param>
+    /// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
+    public static IDisposable Run(Func<bool> action, TimeSpan interval, DispatcherPriority priority = default)
+    {
+        var timer = new DispatcherTimer(priority) { Interval = interval };
 
-        /// <summary>
-        /// Starts a new timer.
-        /// </summary>
-        /// <param name="action">
-        /// The method to call on timer tick. If the method returns false, the timer will stop.
-        /// </param>
-        /// <param name="interval">The interval at which to tick.</param>
-        /// <param name="priority">The priority to use.</param>
-        /// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
-        public static IDisposable Run(Func<bool> action, TimeSpan interval, DispatcherPriority priority = default)
+        timer.Tick += (s, e) =>
         {
-            var timer = new DispatcherTimer(priority) { Interval = interval };
-
-            timer.Tick += (s, e) =>
+            if (!action())
             {
-                if (!action())
-                {
-                    timer.Stop();
-                }
-            };
+                timer.Stop();
+            }
+        };
 
-            timer.Start();
+        timer.Start();
 
-            return Disposable.Create(() => timer.Stop());
-        }
+        return Disposable.Create(() => timer.Stop());
+    }
+    
+    /// <summary>
+    /// Runs a method once, after the specified interval.
+    /// </summary>
+    /// <param name="action">
+    /// The method to call after the interval has elapsed.
+    /// </param>
+    /// <param name="interval">The interval after which to call the method.</param>
+    /// <param name="priority">The priority to use.</param>
+    /// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
+    public static IDisposable RunOnce(
+        Action action,
+        TimeSpan interval,
+        DispatcherPriority priority = default)
+    {
+        interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1);
+            
+        var timer = new DispatcherTimer(priority) { Interval = interval };
 
-        /// <summary>
-        /// Runs a method once, after the specified interval.
-        /// </summary>
-        /// <param name="action">
-        /// The method to call after the interval has elapsed.
-        /// </param>
-        /// <param name="interval">The interval after which to call the method.</param>
-        /// <param name="priority">The priority to use.</param>
-        /// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
-        public static IDisposable RunOnce(
-            Action action,
-            TimeSpan interval,
-            DispatcherPriority priority = default)
+        timer.Tick += (s, e) =>
         {
-            interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1);
-            
-            var timer = new DispatcherTimer(priority) { Interval = interval };
+            action();
+            timer.Stop();
+        };
+
+        timer.Start();
+
+        return Disposable.Create(() => timer.Stop());
+    }
+
+    /// <summary>
+    ///     Occurs when the specified timer interval has elapsed and the
+    ///     timer is enabled.
+    /// </summary>
+    public event EventHandler? Tick;
+
+    /// <summary>
+    ///     Any data that the caller wants to pass along with the timer.
+    /// </summary>
+    public object? Tag { get; set; }
 
-            timer.Tick += (s, e) =>
-            {
-                action();
-                timer.Stop();
-            };
 
-            timer.Start();
+    internal DispatcherTimer(Dispatcher dispatcher, DispatcherPriority priority, TimeSpan interval)
+    {
+        if (dispatcher == null)
+        {
+            throw new ArgumentNullException("dispatcher");
+        }
 
-            return Disposable.Create(() => timer.Stop());
+        DispatcherPriority.Validate(priority, "priority");
+        if (priority == DispatcherPriority.Inactive)
+        {
+            throw new ArgumentException("Specified priority is not valid.", "priority");
         }
 
-        /// <summary>
-        /// Starts the timer.
-        /// </summary>
-        public void Start()
+        if (interval.TotalMilliseconds < 0)
+            throw new ArgumentOutOfRangeException("interval", "TimeSpan period must be greater than or equal to zero.");
+
+        if (interval.TotalMilliseconds > Int32.MaxValue)
+            throw new ArgumentOutOfRangeException("interval",
+                "TimeSpan period must be less than or equal to Int32.MaxValue.");
+
+
+        _dispatcher = dispatcher;
+        _priority = priority;
+        _interval = interval;
+    }
+
+    private void Restart()
+    {
+        lock (_instanceLock)
         {
-            if (!IsEnabled)
+            if (_operation != null)
             {
-                var threading = AvaloniaLocator.Current.GetRequiredService<IPlatformThreadingInterface>();
-                _timer = threading.StartTimer(_priority, Interval, InternalTick);
+                // Timer has already been restarted, e.g. Start was called form the Tick handler.
+                return;
+            }
+
+            // BeginInvoke a new operation.
+            _operation = _dispatcher.InvokeAsync(FireTick, DispatcherPriority.Inactive);
+
+            DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds;
+
+            if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess())
+            {
+                // shortcut - just promote the item now
+                Promote();
+            }
+            else
+            {
+                _dispatcher.AddTimer(this);
             }
         }
+    }
 
-        /// <summary>
-        /// Stops the timer.
-        /// </summary>
-        public void Stop()
+    internal void Promote() // called from Dispatcher
+    {
+        lock (_instanceLock)
         {
-            if (IsEnabled)
+            // Simply promote the operation to it's desired priority.
+            if (_operation != null)
             {
-                _timer!.Dispose();
-                _timer = null;
+                _operation.Priority = _priority;
             }
         }
+    }
 
+    private void FireTick()
+    {
+        // The operation has been invoked, so forget about it.
+        _operation = null;
 
+        // The dispatcher thread is calling us because item's priority
+        // was changed from inactive to something else.
+        if (Tick != null)
+        {
+            Tick(this, EventArgs.Empty);
+        }
 
-        /// <summary>
-        /// Raises the <see cref="Tick"/> event on the dispatcher thread.
-        /// </summary>
-        private void InternalTick()
+        // If we are still enabled, start the timer again.
+        if (_isEnabled)
         {
-            Dispatcher.UIThread.EnsurePriority(_priority);
-            Tick?.Invoke(this, EventArgs.Empty);
+            Restart();
         }
     }
-}
+
+    // This is the object we use to synchronize access.
+    private object _instanceLock = new object();
+
+    // Note: We cannot BE a dispatcher-affinity object because we can be
+    // created by a worker thread.  We are still associated with a
+    // dispatcher (where we post the item) but we can be accessed
+    // by any thread.
+    private Dispatcher _dispatcher;
+
+    private DispatcherPriority _priority; // NOTE: should be Priority
+    private TimeSpan _interval;
+    private DispatcherOperation? _operation;
+    private bool _isEnabled;
+
+    // used by Dispatcher
+    internal long DueTimeInMs { get; private set; }
+}

+ 0 - 25
src/Avalonia.Base/Threading/IDispatcher.cs

@@ -26,30 +26,5 @@ namespace Avalonia.Threading
         /// <param name="action">The method.</param>
         /// <param name="priority">The priority with which to invoke the method.</param>
         void Post(Action action, DispatcherPriority priority = default);
-
-        /// <summary>
-        /// Posts an action that will be invoked on the dispatcher thread.
-        /// </summary>
-        /// <param name="action">The method.</param>
-        /// <param name="arg">The argument of method to call.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default);
-
-        /// <summary>
-        /// Invokes a action on the dispatcher thread.
-        /// </summary>
-        /// <param name="action">The method.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        /// <returns>A task that can be used to track the method's execution.</returns>
-        Task InvokeAsync(Action action, DispatcherPriority priority = default);
-
-        /// <summary>
-        /// Queues the specified work to run on the dispatcher thread and returns a proxy for the
-        /// task returned by <paramref name="function"/>.
-        /// </summary>
-        /// <param name="function">The work to execute asynchronously.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        /// <returns>A task that represents a proxy for the task returned by <paramref name="function"/>.</returns>
-        Task InvokeAsync(Func<Task> function, DispatcherPriority priority = default);
     }
 }

+ 103 - 0
src/Avalonia.Base/Threading/IDispatcherImpl.cs

@@ -0,0 +1,103 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+
+namespace Avalonia.Threading;
+
+[Unstable]
+public interface IDispatcherImpl
+{
+    bool CurrentThreadIsLoopThread { get; }
+
+    // Asynchronously triggers Signaled callback
+    void Signal();
+    event Action Signaled;
+    event Action Timer;
+    long Now { get; }
+    void UpdateTimer(long? dueTimeInMs);
+}
+
+[Unstable]
+public interface IDispatcherImplWithPendingInput : IDispatcherImpl
+{
+    // Checks if dispatcher implementation can 
+    bool CanQueryPendingInput { get; }
+    // Checks if there is pending user input
+    bool HasPendingInput { get; }
+}
+
+[Unstable]
+public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl
+{
+    event Action ReadyForBackgroundProcessing;
+    void RequestBackgroundProcessing();
+}
+
+[Unstable]
+public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput
+{
+    // Runs the event loop
+    void RunLoop(CancellationToken token);
+}
+
+internal class LegacyDispatcherImpl : IDispatcherImpl
+{
+    private readonly IPlatformThreadingInterface _platformThreading;
+    private IDisposable? _timer;
+    private Stopwatch _clock = Stopwatch.StartNew();
+
+    public LegacyDispatcherImpl(IPlatformThreadingInterface platformThreading)
+    {
+        _platformThreading = platformThreading;
+        _platformThreading.Signaled += delegate { Signaled?.Invoke(); };
+    }
+
+    public bool CurrentThreadIsLoopThread => _platformThreading.CurrentThreadIsLoopThread;
+    public void Signal() => _platformThreading.Signal(DispatcherPriority.Send);
+
+    public event Action? Signaled;
+    public event Action? Timer;
+    public long Now => _clock.ElapsedMilliseconds;
+    public void UpdateTimer(long? dueTimeInMs)
+    {
+        _timer?.Dispose();
+        _timer = null;
+
+        if (dueTimeInMs.HasValue)
+        {
+            var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds);
+            _timer = _platformThreading.StartTimer(DispatcherPriority.Send,
+                TimeSpan.FromMilliseconds(interval),
+                OnTick);
+        }
+    }
+
+    private void OnTick()
+    {
+        _timer?.Dispose();
+        _timer = null;
+        Timer?.Invoke();
+    }
+}
+
+class NullDispatcherImpl : IDispatcherImpl
+{
+    public bool CurrentThreadIsLoopThread => true;
+
+    public void Signal()
+    {
+        
+    }
+    
+    public event Action? Signaled;
+    public event Action? Timer;
+
+    public long Now => 0;
+
+    public void UpdateTimer(long? dueTimeInMs)
+    {
+        
+    }
+}

+ 0 - 300
src/Avalonia.Base/Threading/JobRunner.cs

@@ -1,300 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Avalonia.Platform;
-
-namespace Avalonia.Threading
-{
-    /// <summary>
-    /// A main loop in a <see cref="Dispatcher"/>.
-    /// </summary>
-    internal class JobRunner
-    {
-        private IPlatformThreadingInterface? _platform;
-
-        private readonly Queue<IJob>[] _queues = Enumerable.Range(0, (int)DispatcherPriority.MaxValue + 1)
-            .Select(_ => new Queue<IJob>()).ToArray();
-
-        public JobRunner(IPlatformThreadingInterface? platform)
-        {
-            _platform = platform;
-        }
-
-        /// <summary>
-        /// Runs continuations pushed on the loop.
-        /// </summary>
-        /// <param name="priority">Priority to execute jobs for. Pass null if platform doesn't have internal priority system</param>
-        public void RunJobs(DispatcherPriority? priority)
-        {
-            var minimumPriority = priority ?? DispatcherPriority.MinValue;
-            while (true)
-            {
-                var job = GetNextJob(minimumPriority);
-                if (job == null)
-                    return;
-
-                job.Run();
-            }
-        }
-
-        /// <summary>
-        /// Invokes a method on the main loop.
-        /// </summary>
-        /// <param name="action">The method.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        /// <returns>A task that can be used to track the method's execution.</returns>
-        public Task InvokeAsync(Action action, DispatcherPriority priority)
-        {
-            var job = new Job(action, priority, false);
-            AddJob(job);
-            return job.Task!;
-        }
-
-        /// <summary>
-        /// Invokes a method on the main loop.
-        /// </summary>
-        /// <param name="function">The method.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        /// <returns>A task that can be used to track the method's execution.</returns>
-        public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority)
-        {
-            var job = new JobWithResult<TResult>(function, priority);
-            AddJob(job);
-            return job.Task;
-        }
-
-        /// <summary>
-        /// Post action that will be invoked on main thread
-        /// </summary>
-        /// <param name="action">The method.</param>
-        /// 
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        internal void Post(Action action, DispatcherPriority priority)
-        {
-            AddJob(new Job(action, priority, true));
-        }
-
-        /// <summary>
-        /// Post action that will be invoked on main thread
-        /// </summary>
-        /// <param name="action">The method to call.</param>
-        /// <param name="parameter">The parameter of method to call.</param>
-        /// <param name="priority">The priority with which to invoke the method.</param>
-        internal void Post(SendOrPostCallback action, object? parameter, DispatcherPriority priority)
-        {
-            AddJob(new JobWithArg(action, parameter, priority, true));
-        }
-
-        /// <summary>
-        /// Allows unit tests to change the platform threading interface.
-        /// </summary>
-        internal void UpdateServices()
-        {
-            _platform = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>();
-        }
-
-        private void AddJob(IJob job)
-        {
-            bool needWake;
-            var queue = _queues[(int)job.Priority];
-            lock (queue)
-            {
-                needWake = queue.Count == 0;
-                queue.Enqueue(job);
-            }
-            if (needWake)
-                _platform?.Signal(job.Priority);
-        }
-
-        private IJob? GetNextJob(DispatcherPriority minimumPriority)
-        {
-            for (int c = (int)DispatcherPriority.MaxValue; c >= (int)minimumPriority; c--)
-            {
-                var q = _queues[c];
-                lock (q)
-                {
-                    if (q.Count > 0)
-                        return q.Dequeue();
-                }
-            }
-            return null;
-        }
-
-        public bool HasJobsWithPriority(DispatcherPriority minimumPriority)
-        {
-            for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++)
-            {
-                var q = _queues[c];
-                lock (q)
-                {
-                    if (q.Count > 0)
-                        return true;
-                }
-            }
-
-            return false;
-        }
-
-        private interface IJob
-        {
-            /// <summary>
-            /// Gets the job priority.
-            /// </summary>
-            DispatcherPriority Priority { get; }
-
-            /// <summary>
-            /// Runs the job.
-            /// </summary>
-            void Run();
-        }
-
-        /// <summary>
-        /// A job to run.
-        /// </summary>
-        private sealed class Job : IJob
-        {
-            /// <summary>
-            /// The method to call.
-            /// </summary>
-            private readonly Action _action;
-            /// <summary>
-            /// The task completion source.
-            /// </summary>
-            private readonly TaskCompletionSource<object?>? _taskCompletionSource;
-
-            /// <summary>
-            /// Initializes a new instance of the <see cref="Job"/> class.
-            /// </summary>
-            /// <param name="action">The method to call.</param>
-            /// <param name="priority">The job priority.</param>
-            /// <param name="throwOnUiThread">Do not wrap exception in TaskCompletionSource</param>
-            public Job(Action action, DispatcherPriority priority, bool throwOnUiThread)
-            {
-                _action = action;
-                Priority = priority;
-                _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource<object?>();
-            }
-
-            /// <inheritdoc/>
-            public DispatcherPriority Priority { get; }
-
-            /// <summary>
-            /// The task.
-            /// </summary>
-            public Task? Task => _taskCompletionSource?.Task;
-            
-            /// <inheritdoc/>
-            void IJob.Run()
-            {
-                if (_taskCompletionSource == null)
-                {
-                    _action();
-                    return;
-                }
-                try
-                {
-                    _action();
-                    _taskCompletionSource.SetResult(null);
-                }
-                catch (Exception e)
-                {
-                    _taskCompletionSource.SetException(e);
-                }
-            }
-        }
-
-        /// <summary>
-        /// A typed job to run.
-        /// </summary>
-        private sealed class JobWithArg : IJob
-        {
-            private readonly SendOrPostCallback _action;
-            private readonly object? _parameter;
-            private readonly TaskCompletionSource<bool>? _taskCompletionSource;
-
-            /// <summary>
-            /// Initializes a new instance of the <see cref="Job"/> class.
-            /// </summary>
-            /// <param name="action">The method to call.</param>
-            /// <param name="parameter">The parameter of method to call.</param>
-            /// <param name="priority">The job priority.</param>
-            /// <param name="throwOnUiThread">Do not wrap exception in TaskCompletionSource</param>
-
-            public JobWithArg(SendOrPostCallback action, object? parameter, DispatcherPriority priority, bool throwOnUiThread)
-            {
-                _action = action;
-                _parameter = parameter;
-                Priority = priority;
-                _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource<bool>();
-            }
-
-            /// <inheritdoc/>
-            public DispatcherPriority Priority { get; }
-
-            /// <inheritdoc/>
-            void IJob.Run()
-            {
-                if (_taskCompletionSource == null)
-                {
-                    _action(_parameter);
-                    return;
-                }
-                try
-                {
-                    _action(_parameter);
-                    _taskCompletionSource.SetResult(default);
-                }
-                catch (Exception e)
-                {
-                    _taskCompletionSource.SetException(e);
-                }
-            }
-        }
-
-        /// <summary>
-        /// A job to run thath return value.
-        /// </summary>
-        /// <typeparam name="TResult">Type of job result</typeparam>
-        private sealed class JobWithResult<TResult> : IJob
-        {
-            private readonly Func<TResult> _function;
-            private readonly TaskCompletionSource<TResult> _taskCompletionSource;
-
-            /// <summary>
-            /// Initializes a new instance of the <see cref="Job"/> class.
-            /// </summary>
-            /// <param name="function">The method to call.</param>
-            /// <param name="priority">The job priority.</param>
-            public JobWithResult(Func<TResult> function, DispatcherPriority priority)
-            {
-                _function = function;
-                Priority = priority;
-                _taskCompletionSource = new TaskCompletionSource<TResult>();
-            }
-
-            /// <inheritdoc/>
-            public DispatcherPriority Priority { get; }
-
-            /// <summary>
-            /// The task.
-            /// </summary>
-            public Task<TResult> Task => _taskCompletionSource.Task;
-
-            /// <inheritdoc/>
-            void IJob.Run()
-            {
-                try
-                {
-                    var result = _function();
-                    _taskCompletionSource.SetResult(result);
-                }
-                catch (Exception e)
-                {
-                    _taskCompletionSource.SetException(e);
-                }
-            }
-        }
-    }
-}

+ 0 - 92
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@@ -1,92 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using System.Threading;
-using Avalonia.Metadata;
-using Avalonia.Platform;
-using Avalonia.Threading;
-
-namespace Avalonia.Controls.Platform
-{
-    [Unstable]
-    public class InternalPlatformThreadingInterface : IPlatformThreadingInterface
-    {
-        public InternalPlatformThreadingInterface()
-        {
-            TlsCurrentThreadIsLoopThread = true;
-        }
-
-        private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
-
-
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            var handles = new[] { _signaled, cancellationToken.WaitHandle };
-
-            while (!cancellationToken.IsCancellationRequested)
-            {
-                Signaled?.Invoke(null);
-                WaitHandle.WaitAny(handles);
-            }
-        }
-
-
-        class TimerImpl : IDisposable
-        {
-            private readonly DispatcherPriority _priority;
-            private readonly TimeSpan _interval;
-            private readonly Action _tick;
-            private Timer? _timer;
-            private GCHandle _handle;
-
-            public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick)
-            {
-                _priority = priority;
-                _interval = interval;
-                _tick = tick;
-                _timer = new Timer(OnTimer, null, interval, Timeout.InfiniteTimeSpan);
-                _handle = GCHandle.Alloc(_timer);
-            }
-
-            private void OnTimer(object? state)
-            {
-                if (_timer == null)
-                    return;
-                Dispatcher.UIThread.Post(() =>
-                {
-                    
-                    if (_timer == null)
-                        return;
-                    _tick();
-                    _timer?.Change(_interval, Timeout.InfiniteTimeSpan);
-                });
-            }
-
-
-            public void Dispose()
-            {
-                _handle.Free();
-                _timer?.Dispose();
-                _timer = null;
-            }
-        }
-
-        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
-        {
-            return new TimerImpl(priority, interval, tick);
-        }
-
-        public void Signal(DispatcherPriority prio)
-        {
-            _signaled.Set();
-        }
-
-        [ThreadStatic] private static bool TlsCurrentThreadIsLoopThread;
-
-        public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread;
-        public event Action<DispatcherPriority?>? Signaled;
-#pragma warning disable CS0067
-        public event Action<TimeSpan>? Tick;
-#pragma warning restore CS0067
-
-    }
-}

+ 109 - 0
src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs

@@ -0,0 +1,109 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using Avalonia.Metadata;
+using Avalonia.Threading;
+
+namespace Avalonia.Controls.Platform;
+
+[Unstable]
+public class ManagedDispatcherImpl : IControlledDispatcherImpl
+{
+    private readonly IManagedDispatcherInputProvider? _inputProvider;
+    private readonly AutoResetEvent _wakeup = new(false);
+    private bool _signaled;
+    private readonly object _lock = new();
+    private readonly Stopwatch _clock = Stopwatch.StartNew();
+    private TimeSpan? _nextTimer; 
+    private readonly Thread _loopThread = Thread.CurrentThread;
+
+    public interface IManagedDispatcherInputProvider
+    {
+        bool HasInput { get; }
+        void DispatchNextInputEvent();
+    }
+
+    public ManagedDispatcherImpl(IManagedDispatcherInputProvider? inputProvider)
+    {
+        _inputProvider = inputProvider;
+    }
+
+    public bool CurrentThreadIsLoopThread => _loopThread == Thread.CurrentThread;
+    public void Signal()
+    {
+        lock (_lock)
+        {
+            _signaled = true;
+            _wakeup.Set();
+        }
+    }
+
+    public event Action? Signaled;
+    public event Action? Timer;
+    public long Now => _clock.ElapsedMilliseconds;
+    public void UpdateTimer(long? dueTimeInMs)
+    {
+        lock (_lock)
+        {
+            _nextTimer = dueTimeInMs == null
+                ? null
+                : TimeSpan.FromMilliseconds(dueTimeInMs.Value);
+            if (!CurrentThreadIsLoopThread)
+                _wakeup.Set();
+        }
+    }
+
+    public bool CanQueryPendingInput => _inputProvider != null;
+    public bool HasPendingInput => _inputProvider?.HasInput ?? false;
+    
+    public void RunLoop(CancellationToken token)
+    {
+        while (!token.IsCancellationRequested)
+        {
+            bool signaled;
+            lock (_lock)
+            {
+                signaled = _signaled;
+                _signaled = false;
+            }
+
+            if (signaled)
+            {
+                Signaled?.Invoke();
+                continue;
+            }
+
+            bool fireTimer = false;
+            lock (_lock)
+            {
+                if (_nextTimer < _clock.Elapsed)
+                {
+                    fireTimer = true;
+                    _nextTimer = null;
+                }
+            }
+
+            if (fireTimer)
+            {
+                Timer?.Invoke();
+                continue;
+            }
+
+            if (_inputProvider?.HasInput == true)
+            {
+                _inputProvider.DispatchNextInputEvent();
+                continue;
+            }
+
+            if (_nextTimer != null)
+            {
+                var waitFor = _clock.Elapsed - _nextTimer.Value;
+                if (waitFor.TotalMilliseconds < 1)
+                    continue;
+                _wakeup.WaitOne(waitFor);
+            }
+            else
+                _wakeup.WaitOne();
+        }
+    }
+}

+ 2 - 2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@@ -6,6 +6,7 @@ using Avalonia.Input.Platform;
 using Avalonia.Platform;
 using Avalonia.Remote.Protocol;
 using Avalonia.Rendering;
+using Avalonia.Threading;
 
 namespace Avalonia.DesignerSupport.Remote
 {
@@ -46,13 +47,12 @@ namespace Avalonia.DesignerSupport.Remote
         {
             s_transport = transport;
             var instance = new PreviewerWindowingPlatform();
-            var threading = new InternalPlatformThreadingInterface();
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToSingleton<ClipboardStub>()
                 .Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(Keyboard)
                 .Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
-                .Bind<IPlatformThreadingInterface>().ToConstant(threading)
+                .Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<IWindowingPlatform>().ToConstant(instance)

+ 1 - 1
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@@ -60,7 +60,7 @@ namespace Avalonia.Headless
         internal static void Initialize(AvaloniaHeadlessPlatformOptions opts)
         {
             AvaloniaLocator.CurrentMutable
-                .Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface())
+                .Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
                 .Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
                 .Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
                 .Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()

+ 0 - 86
src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs

@@ -1,86 +0,0 @@
-using System;
-using Avalonia.Reactive;
-using System.Threading;
-using Avalonia.Platform;
-using Avalonia.Threading;
-
-namespace Avalonia.Headless
-{
-    class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface
-    {
-        public HeadlessPlatformThreadingInterface()
-        {
-            _thread = Thread.CurrentThread;
-        }
-        
-        private AutoResetEvent _event = new AutoResetEvent(false);
-        private Thread _thread;
-        private object _lock = new object();
-        private DispatcherPriority? _signaledPriority;
-
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            while (!cancellationToken.IsCancellationRequested)
-            {
-                DispatcherPriority? signaled = null;
-                lock (_lock)
-                {
-                    signaled = _signaledPriority;
-                    _signaledPriority = null;
-                }
-                if(signaled.HasValue)
-                    Signaled?.Invoke(signaled);
-                WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20));
-            }
-        }
-
-        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
-        {
-            if (interval.TotalMilliseconds < 10)
-                interval = TimeSpan.FromMilliseconds(10);
-
-            var stopped = false;
-            Timer timer = null;
-            timer = new Timer(_ =>
-            {
-                if (stopped)
-                    return;
-
-                Dispatcher.UIThread.Post(() =>
-                {
-                    try
-                    {
-                        tick();
-                    }
-                    finally
-                    {
-                        if (!stopped)
-                            timer.Change(interval, Timeout.InfiniteTimeSpan);
-                    }
-                });
-            },
-            null, interval, Timeout.InfiniteTimeSpan);
-
-            return Disposable.Create(() =>
-            {
-                stopped = true;
-                timer.Dispose();
-            });
-        }
-
-        public void Signal(DispatcherPriority priority)
-        {
-            lock (_lock)
-            {
-                if (_signaledPriority == null || _signaledPriority.Value > priority)
-                {
-                    _signaledPriority = priority;
-                }
-                _event.Set();
-            }
-        }
-
-        public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread;
-        public event Action<DispatcherPriority?> Signaled;
-    }
-}

+ 3 - 2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -9,6 +9,7 @@ using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
+using Avalonia.Threading;
 using MicroCom.Runtime;
 #nullable enable
 
@@ -98,8 +99,8 @@ namespace Avalonia.Native
             }
 
             AvaloniaLocator.CurrentMutable
-                .Bind<IPlatformThreadingInterface>()
-                .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface()))
+                .Bind<IDispatcherImpl>()
+                .ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface()))
                 .Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
                 .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)

+ 3 - 4
src/Avalonia.Native/CallbackBase.cs

@@ -2,6 +2,7 @@
 using System.Runtime.ExceptionServices;
 using Avalonia.MicroCom;
 using Avalonia.Platform;
+using Avalonia.Threading;
 using MicroCom.Runtime;
 
 namespace Avalonia.Native
@@ -10,11 +11,9 @@ namespace Avalonia.Native
     {
         public void RaiseException(Exception e)
         {
-            if (AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() is PlatformThreadingInterface threadingInterface)
+            if (AvaloniaLocator.Current.GetService<IDispatcherImpl>() is DispatcherImpl dispatcherImpl)
             {
-                threadingInterface.TerminateNativeApp();
-
-                threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e));
+                dispatcherImpl.PropagateCallbackException(ExceptionDispatchInfo.Capture(e));
             }
         }
     }

+ 132 - 0
src/Avalonia.Native/DispatcherImpl.cs

@@ -0,0 +1,132 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using Avalonia.Native.Interop;
+using Avalonia.Threading;
+using MicroCom.Runtime;
+
+namespace Avalonia.Native;
+
+internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing
+{
+    private readonly IAvnPlatformThreadingInterface _native;
+    private Thread? _loopThread;
+    private Stopwatch _clock = Stopwatch.StartNew();
+    private Stack<RunLoopFrame> _managedFrames = new();
+
+    public DispatcherImpl(IAvnPlatformThreadingInterface native)
+    {
+        _native = native;
+        using var events = new Events(this);
+        _native.SetEvents(events);
+    }
+    
+    public event Action Signaled;
+    public event Action Timer;
+    public event Action ReadyForBackgroundProcessing;
+    
+    private class Events : NativeCallbackBase, IAvnPlatformThreadingInterfaceEvents
+    {
+        private readonly DispatcherImpl _parent;
+
+        public Events(DispatcherImpl parent)
+        {
+            _parent = parent;
+        }
+        public void Signaled() => _parent.Signaled?.Invoke();
+
+        public void Timer() => _parent.Timer?.Invoke();
+
+        public void ReadyForBackgroundProcessing() => _parent.ReadyForBackgroundProcessing?.Invoke();
+    }
+
+    public bool CurrentThreadIsLoopThread
+    {
+        get
+        {
+            if (_loopThread != null)
+                return Thread.CurrentThread == _loopThread;
+            if (_native.CurrentThreadIsLoopThread == 0)
+                return false;
+            _loopThread = Thread.CurrentThread;
+            return true;
+        }
+    }
+
+    public void Signal() => _native.Signal();
+
+    public void UpdateTimer(long? dueTimeInMs)
+    {
+        var ms = dueTimeInMs == null ? -1 : (int)Math.Min(int.MaxValue - 10, Math.Max(1, dueTimeInMs.Value - Now));
+        _native.UpdateTimer(ms);
+    }
+
+    public bool CanQueryPendingInput => false;
+    public bool HasPendingInput => false;
+
+    class RunLoopFrame : IDisposable
+    {
+        public ExceptionDispatchInfo? Exception;
+        public CancellationTokenSource CancellationTokenSource = new();
+
+        public RunLoopFrame(CancellationToken token)
+        {
+            CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
+        }
+
+        public void Dispose() => CancellationTokenSource.Dispose();
+    }
+    
+    public void RunLoop(CancellationToken token)
+    {
+        if (token.IsCancellationRequested)
+            return;
+        object l = new();
+        var exited = false;
+        
+        using var frame = new RunLoopFrame(token);
+        
+        using var cancel = _native.CreateLoopCancellation();
+        frame.CancellationTokenSource.Token.Register(() =>
+        {
+            lock (l)
+                // ReSharper disable once AccessToModifiedClosure
+                // ReSharper disable once AccessToDisposedClosure
+                if (!exited)
+                    cancel.Cancel();
+        });
+        
+        try
+        {
+            _managedFrames.Push(frame);
+            _native.RunLoop(cancel);
+        }
+        finally
+        {
+            lock (l)
+                exited = true;
+            _managedFrames.Pop();
+            if (frame.Exception != null)
+                frame.Exception.Throw();
+        }
+    }
+
+    public long Now => _clock.ElapsedMilliseconds;
+
+    public void PropagateCallbackException(ExceptionDispatchInfo capture)
+    {
+        if (_managedFrames.Count == 0)
+        {
+            Debug.Assert(false, "We should never get here");
+            return;
+        }
+
+        var frame = _managedFrames.Peek();
+        frame.Exception = capture;
+        frame.CancellationTokenSource.Cancel();
+    }
+    public void RequestBackgroundProcessing() => _native.RequestBackgroundProcessing();
+}

+ 0 - 115
src/Avalonia.Native/PlatformThreadingInterface.cs

@@ -1,115 +0,0 @@
-using System;
-using System.Runtime.ExceptionServices;
-using System.Threading;
-using Avalonia.Native.Interop;
-using Avalonia.Platform;
-using Avalonia.Threading;
-
-namespace Avalonia.Native
-{
-    internal class PlatformThreadingInterface : IPlatformThreadingInterface
-    {
-        class TimerCallback : NativeCallbackBase, IAvnActionCallback
-        {
-            readonly Action _tick;
-
-            public TimerCallback(Action tick)
-            {
-                _tick = tick;
-            }
-
-            public void Run()
-            {
-                _tick();
-            }
-        }
-
-        class SignaledCallback : NativeCallbackBase, IAvnSignaledCallback
-        {
-            readonly PlatformThreadingInterface _parent;
-
-            public SignaledCallback(PlatformThreadingInterface parent)
-            {
-                _parent = parent;
-            }
-
-            public void Signaled(int priority, int priorityContainsMeaningfulValue)
-            {
-                _parent.Signaled?.Invoke(priorityContainsMeaningfulValue.FromComBool() ? (DispatcherPriority?)priority : null);
-            }
-        }
-
-        readonly IAvnPlatformThreadingInterface _native;
-        private ExceptionDispatchInfo _exceptionDispatchInfo;
-        private CancellationTokenSource _exceptionCancellationSource;
-
-        public PlatformThreadingInterface(IAvnPlatformThreadingInterface native)
-        {
-            _native = native;
-            using (var cb = new SignaledCallback(this))
-                _native.SetSignaledCallback(cb);
-        }
-
-        public bool CurrentThreadIsLoopThread => _native.CurrentThreadIsLoopThread.FromComBool();
-
-        public event Action<DispatcherPriority?> Signaled;
-
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            _exceptionDispatchInfo?.Throw();
-            var l = new object();
-            _exceptionCancellationSource = new CancellationTokenSource();
-
-            var compositeCancellation = CancellationTokenSource
-                .CreateLinkedTokenSource(cancellationToken, _exceptionCancellationSource.Token).Token;
-
-            var cancellation = _native.CreateLoopCancellation();
-            compositeCancellation.Register(() =>
-            {
-                lock (l)
-                {
-                    cancellation?.Cancel();
-                }
-            });
-
-            try
-            {
-                _native.RunLoop(cancellation);
-            }
-            finally
-            {
-                lock (l)
-                {
-                    cancellation?.Dispose();
-                    cancellation = null;
-                }
-            }
-
-            if (_exceptionDispatchInfo != null)
-            {
-                _exceptionDispatchInfo.Throw();
-            }
-        }
-
-        public void DispatchException (ExceptionDispatchInfo exceptionInfo)
-        {
-            _exceptionDispatchInfo = exceptionInfo;
-        }
-
-        public void TerminateNativeApp()
-        {
-            _exceptionCancellationSource?.Cancel();
-        }
-
-        public void Signal(DispatcherPriority priority)
-        {
-            _native.Signal((int)priority);
-        }
-
-        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
-        {
-            using (var cb = new TimerCallback(tick))
-                return _native.StartTimer((int)priority, (int)interval.TotalMilliseconds, cb);
-        }
-    }
-}

+ 9 - 7
src/Avalonia.Native/avn.idl

@@ -645,9 +645,11 @@ interface IAvnActionCallback : IUnknown
 }
 
 [uuid(6df4d2db-0b80-4f59-ad88-0baa5e21eb14)]
-interface IAvnSignaledCallback : IUnknown
+interface IAvnPlatformThreadingInterfaceEvents : IUnknown
 {
-     void Signaled(int priority, bool priorityContainsMeaningfulValue);
+     void Signaled();
+     void Timer();
+     void ReadyForBackgroundProcessing();
 }
 
 [uuid(97330f88-c22b-4a8e-a130-201520091b01)]
@@ -660,12 +662,12 @@ interface IAvnLoopCancellation : IUnknown
 interface IAvnPlatformThreadingInterface : IUnknown
 {
      bool GetCurrentThreadIsLoopThread();
-     void SetSignaledCallback(IAvnSignaledCallback* cb);
+     void SetEvents(IAvnPlatformThreadingInterfaceEvents* cb);
      IAvnLoopCancellation* CreateLoopCancellation();
-     HRESULT RunLoop(IAvnLoopCancellation* cancel);
-    // Can't pass int* to sharpgentools for some reason
-     void Signal(int priority);
-     IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback);
+     void RunLoop(IAvnLoopCancellation* cancel);
+     void Signal();
+     void UpdateTimer(int ms);
+     void RequestBackgroundProcessing();
 }
 
 [uuid(6c621a6e-e4c1-4ae3-9749-83eeeffa09b6)]

+ 3 - 1
src/Avalonia.X11/X11Platform.cs

@@ -14,6 +14,7 @@ using Avalonia.OpenGL.Egl;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
+using Avalonia.Threading;
 using Avalonia.X11;
 using Avalonia.X11.Glx;
 using static Avalonia.X11.XLib;
@@ -34,6 +35,7 @@ namespace Avalonia.X11
         public X11PlatformOptions Options { get; private set; }
         public IntPtr OrphanedWindow { get; private set; }
         public X11Globals Globals { get; private set; }
+        public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new();
         [DllImport("libc")]
         private static extern void setlocale(int type, string s);
         public void Initialize(X11PlatformOptions options)
@@ -72,7 +74,7 @@ namespace Avalonia.X11
 
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IWindowingPlatform>().ToConstant(this)
-                .Bind<IPlatformThreadingInterface>().ToConstant(new X11PlatformThreading(this))
+                .Bind<IDispatcherImpl>().ToConstant(new X11PlatformThreading(this))
                 .Bind<IRenderTimer>().ToConstant(new SleepLoopRenderTimer(60))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))

+ 59 - 100
src/Avalonia.X11/X11PlatformThreading.cs

@@ -9,7 +9,7 @@ using static Avalonia.X11.XLib;
 
 namespace Avalonia.X11
 {
-    internal unsafe class X11PlatformThreading : IPlatformThreadingInterface
+    internal unsafe class X11PlatformThreading : IControlledDispatcherImpl
     {
         private readonly AvaloniaX11Platform _platform;
         private readonly IntPtr _display;
@@ -68,44 +68,11 @@ namespace Avalonia.X11
         private int _sigread, _sigwrite;
         private object _lock = new object();
         private bool _signaled;
-        private DispatcherPriority _signaledPriority;
+        private bool _wakeupRequested;
+        private long? _nextTimer;
         private int _epoll;
         private Stopwatch _clock = Stopwatch.StartNew();
 
-        private class X11Timer : IDisposable
-        {
-            private readonly X11PlatformThreading _parent;
-
-            public X11Timer(X11PlatformThreading parent, DispatcherPriority prio, TimeSpan interval, Action tick)
-            {
-                _parent = parent;
-                Priority = prio;
-                Tick = tick;
-                Interval = interval;
-                Reschedule();
-            }
-            
-            public DispatcherPriority Priority { get; }
-            public TimeSpan NextTick { get; private set; }
-            public TimeSpan Interval { get; }
-            public Action Tick { get; }
-            public bool Disposed { get; private set; }
-
-            public void Reschedule()
-            {
-                NextTick = _parent._clock.Elapsed + Interval;
-            }
-
-            public void Dispose()
-            {
-                Disposed = true;
-                lock (_parent._lock)
-                    _parent._timers.Remove(this);
-            }
-        }
-
-        private List<X11Timer> _timers = new List<X11Timer>();
-        
         public X11PlatformThreading(AvaloniaX11Platform platform)
         {
             _platform = platform;
@@ -139,29 +106,16 @@ namespace Avalonia.X11
                 throw new X11Exception("Unable to attach signal pipe to epoll");
         }
 
-        private int TimerComparer(X11Timer t1, X11Timer t2)
-        {
-            return t2.Priority - t1.Priority;
-        }
-
         private void CheckSignaled()
         {
-            int buf = 0;
-            while (read(_sigread, &buf, new IntPtr(4)).ToInt64() > 0)
-            {
-            }
-
-            DispatcherPriority prio;
             lock (_lock)
             {
                 if (!_signaled)
                     return;
                 _signaled = false;
-                prio = _signaledPriority;
-                _signaledPriority = DispatcherPriority.MinValue;
             }
 
-            Signaled?.Invoke(prio);
+            Signaled?.Invoke();
         }
 
         private unsafe void HandleX11(CancellationToken cancellationToken)
@@ -170,6 +124,7 @@ namespace Avalonia.X11
             {
                 if (cancellationToken.IsCancellationRequested)
                     return;
+                
                 XNextEvent(_display, out var xev);
                 if(XFilterEvent(ref xev, IntPtr.Zero))
                     continue;
@@ -195,90 +150,94 @@ namespace Avalonia.X11
                         XFreeEventData(_display, &xev.GenericEventCookie);
                 }
             }
-
-            Dispatcher.UIThread.RunJobs();
         }
-        
+
         public void RunLoop(CancellationToken cancellationToken)
         {
-            var readyTimers = new List<X11Timer>();
             while (!cancellationToken.IsCancellationRequested)
             {
-                var now = _clock.Elapsed;
-                TimeSpan? nextTick = null;
-                readyTimers.Clear();
-                lock(_timers)
-                    foreach (var t in _timers)
-                    {
-                        if (nextTick == null || t.NextTick < nextTick.Value)
-                            nextTick = t.NextTick;
-                        if (t.NextTick < now)
-                            readyTimers.Add(t);
-                    }
-                
-                readyTimers.Sort(TimerComparer);
-                
-                foreach (var t in readyTimers)
+                var now = _clock.ElapsedMilliseconds;
+                if (_nextTimer.HasValue && now > _nextTimer.Value)
                 {
-                    if (cancellationToken.IsCancellationRequested)
-                        return;
-                    t.Tick();
-                    if(!t.Disposed)
-                    {
-                        t.Reschedule();
-                        if (nextTick == null || t.NextTick < nextTick.Value)
-                            nextTick = t.NextTick;
-                    }
+                    Timer?.Invoke();
                 }
 
                 if (cancellationToken.IsCancellationRequested)
                     return;
+                
                 //Flush whatever requests were made to XServer
                 XFlush(_display);
                 epoll_event ev;
                 if (XPending(_display) == 0)
-                    epoll_wait(_epoll, &ev, 1,
-                        nextTick == null ? -1 : Math.Max(1, (int)(nextTick.Value - _clock.Elapsed).TotalMilliseconds));
+                {
+                    now = _clock.ElapsedMilliseconds;
+                    if (_nextTimer < now)
+                        continue;
+                    
+                    var timeout = _nextTimer == null ? (int)-1 : Math.Max(1, _nextTimer.Value - now);
+                    epoll_wait(_epoll, &ev, 1, (int)Math.Min(int.MaxValue, timeout));
+                    
+                    // Drain the signaled pipe
+                    int buf = 0;
+                    while (read(_sigread, &buf, new IntPtr(4)).ToInt64() > 0)
+                    {
+                    }
+
+                    lock (_lock)
+                        _wakeupRequested = false;
+                }
+
                 if (cancellationToken.IsCancellationRequested)
                     return;
                 CheckSignaled();
                 HandleX11(cancellationToken);
+                while (_platform.EventGrouperDispatchQueue.HasJobs)
+                {
+                    CheckSignaled();
+                    _platform.EventGrouperDispatchQueue.DispatchNext();
+                }
             }
         }
 
-        
+        private void Wakeup()
+        {
+            lock (_lock)
+            {
+                if(_wakeupRequested)
+                    return;
+                _wakeupRequested = true;
+                int buf = 0;
+                write(_sigwrite, &buf, new IntPtr(1));
+            }
+        }
 
-        public void Signal(DispatcherPriority priority)
+        public void Signal()
         {
             lock (_lock)
             {
-                if (priority > _signaledPriority)
-                    _signaledPriority = priority;
-                
                 if(_signaled)
                     return;
                 _signaled = true;
-                int buf = 0;
-                write(_sigwrite, &buf, new IntPtr(1));
+                Wakeup();
             }
         }
 
         public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _mainThread;
-        public event Action<DispatcherPriority?> Signaled;
         
-        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
+        public event Action Signaled;
+        public event Action Timer;
+
+        public void UpdateTimer(long? dueTimeInMs)
         {
-            if (_mainThread != Thread.CurrentThread)
-                throw new InvalidOperationException("StartTimer can be only called from UI thread");
-            if (interval <= TimeSpan.Zero)
-                throw new ArgumentException("Interval must be positive", nameof(interval));
-            
-            // We assume that we are on the main thread and outside of epoll_wait, so there is no need for wakeup signal
-            
-            var timer = new X11Timer(this, priority, interval, tick);
-            lock(_timers)
-                _timers.Add(timer);
-            return timer;
+            _nextTimer = dueTimeInMs;
+            if (_nextTimer != null)
+                Wakeup();
         }
+
+
+        public long Now => (int)_clock.ElapsedMilliseconds;
+        public bool CanQueryPendingInput => true;
+
+        public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0;
     }
 }

+ 1 - 2
src/Avalonia.X11/X11Window.cs

@@ -190,7 +190,7 @@ namespace Avalonia.X11
             UpdateMotifHints();
             UpdateSizeHints(null);
 
-            _rawEventGrouper = new RawEventGrouper(DispatchInput);
+            _rawEventGrouper = new RawEventGrouper(DispatchInput, platform.EventGrouperDispatchQueue);
             
             _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
             _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None);
@@ -515,7 +515,6 @@ namespace Avalonia.X11
                         if (changedSize && !updatedSizeViaScaling && !_popup)
                             Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified);
 
-                        Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
                     }, DispatcherPriority.Layout);
                 if (_useRenderWindow)
                     XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width,

+ 0 - 5
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@@ -49,11 +49,6 @@ namespace Avalonia.Browser
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
         }
 
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            throw new NotSupportedException();
-        }
-
         public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
         {
             return GetRuntimePlatform()

+ 7 - 2
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@@ -8,13 +8,15 @@ using Avalonia.LinuxFramebuffer.Output;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
+ using Avalonia.Threading;
 
-namespace Avalonia.LinuxFramebuffer
+ namespace Avalonia.LinuxFramebuffer
 {
     class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider
     {
         private readonly IOutputBackend _outputBackend;
         private readonly IInputBackend _inputBackend;
+        private readonly RawEventGrouper _inputQueue;
 
         public IInputRoot InputRoot { get; private set; }
 
@@ -22,9 +24,12 @@ namespace Avalonia.LinuxFramebuffer
         {
             _outputBackend = outputBackend;
             _inputBackend = inputBackend;
+            _inputQueue = new RawEventGrouper(groupedInput => Input?.Invoke(groupedInput),
+                LinuxFramebufferPlatform.EventGrouperDispatchQueue);
 
             Surfaces = new object[] { _outputBackend };
-            _inputBackend.Initialize(this, e => Input?.Invoke(e));
+            _inputBackend.Initialize(this, e =>
+                Dispatcher.UIThread.Post(() => _inputQueue.HandleEvent(e), DispatcherPriority.Send ));
         }
 
         public IRenderer CreateRenderer(IRenderRoot root)

+ 3 - 8
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs

@@ -14,12 +14,10 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
         private int _epoll;
         private Action<RawInputEventArgs> _onInput;
         private IInputRoot _inputRoot;
-        private RawEventGroupingThreadingHelper _inputQueue;
 
         public EvDevBackend(EvDevDeviceDescription[] devices)
         {
             _deviceDescriptions = devices;
-            _inputQueue = new RawEventGroupingThreadingHelper(e => _onInput?.Invoke(e));
         }
         
         unsafe void InputThread()
@@ -45,12 +43,9 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
             }
         }
 
-        private void OnRawEvent(RawInputEventArgs obj)
-        {
-            _inputQueue.OnEvent(obj);
-        }
-        
-        
+        private void OnRawEvent(RawInputEventArgs obj) => _onInput?.Invoke(obj);
+
+
         public void Initialize(IScreenInfoProvider info, Action<RawInputEventArgs> onInput)
         {
             _onInput = onInput;

+ 1 - 3
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs

@@ -13,14 +13,12 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
         private IInputRoot _inputRoot;
         private TouchDevice _touch = new TouchDevice();
         private const string LibInput = nameof(Avalonia.LinuxFramebuffer) + "/" + nameof(Avalonia.LinuxFramebuffer.Input) + "/" + nameof(LibInput);
-        private readonly RawEventGroupingThreadingHelper _inputQueue;
         private Action<RawInputEventArgs> _onInput;
         private Dictionary<int, Point> _pointers = new Dictionary<int, Point>();
 
         public LibInputBackend()
         {
             var ctx = libinput_path_create_context();
-            _inputQueue = new(e => _onInput?.Invoke(e));
             new Thread(() => InputThread(ctx)).Start();
         }
 
@@ -58,7 +56,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
             }
         }
 
-        private void ScheduleInput(RawInputEventArgs ev) => _inputQueue.OnEvent(ev);
+        private void ScheduleInput(RawInputEventArgs ev) => _onInput.Invoke(ev);
 
         private void HandleTouch(IntPtr ev, LibInputEventType type)
         {

+ 5 - 7
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -16,6 +16,8 @@ using Avalonia.LinuxFramebuffer.Output;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
+using Avalonia.Threading;
+
 #nullable enable
 
 namespace Avalonia.LinuxFramebuffer
@@ -23,9 +25,7 @@ namespace Avalonia.LinuxFramebuffer
     class LinuxFramebufferPlatform
     {
         IOutputBackend _fb;
-        private static readonly Stopwatch St = Stopwatch.StartNew();
-        internal static uint Timestamp => (uint)St.ElapsedTicks;
-        public static InternalPlatformThreadingInterface? Threading;
+        public static ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue = new();
 
         internal static Compositor Compositor { get; private set; } = null!;
         
@@ -34,18 +34,16 @@ namespace Avalonia.LinuxFramebuffer
         {
             _fb = backend;
         }
-
-
+        
         void Initialize()
         {
-            Threading = new InternalPlatformThreadingInterface();
             if (_fb is IGlOutputBackend gl)
                 AvaloniaLocator.CurrentMutable.Bind<IPlatformGraphics>().ToConstant(gl.PlatformGraphics);
 
             var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>() ?? new LinuxFramebufferPlatformOptions();
 
             AvaloniaLocator.CurrentMutable
-                .Bind<IPlatformThreadingInterface>().ToConstant(Threading)
+                .Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)))
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts.Fps))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()

+ 87 - 22
src/Shared/RawEventGrouping.cs

@@ -2,6 +2,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Collections.Pooled;
+using Avalonia.Controls.Platform;
 using Avalonia.Input.Raw;
 using Avalonia.Threading;
 
@@ -12,28 +13,60 @@ namespace Avalonia;
   While doing that it groups Move and TouchUpdate events so we could provide GetIntermediatePoints API
  */
 
-internal class RawEventGrouper : IDisposable
+internal interface IRawEventGrouperDispatchQueue
 {
-    private readonly Action<RawInputEventArgs> _eventCallback;
-    private readonly Queue<RawInputEventArgs> _inputQueue = new();
+    void Add(RawInputEventArgs args, Action<RawInputEventArgs> handler);
+}
+
+
+class ManualRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue
+{
+    private readonly Queue<(RawInputEventArgs args, Action<RawInputEventArgs> handler)> _inputQueue = new();
+    public void Add(RawInputEventArgs args, Action<RawInputEventArgs> handler) => _inputQueue.Enqueue((args, handler));
+
+    public bool HasJobs => _inputQueue.Count > 0;
+    
+    public void DispatchNext()
+    {
+        if (_inputQueue.Count == 0)
+            return;
+        var ev = _inputQueue.Dequeue();
+        ev.handler(ev.args);
+    }
+}
+
+internal class ManualRawEventGrouperDispatchQueueDispatcherInputProvider : ManagedDispatcherImpl.IManagedDispatcherInputProvider
+{
+    private readonly ManualRawEventGrouperDispatchQueue _queue;
+
+    public ManualRawEventGrouperDispatchQueueDispatcherInputProvider(ManualRawEventGrouperDispatchQueue queue)
+    {
+        _queue = queue;
+    }
+
+    public bool HasInput => _queue.HasJobs;
+    public void DispatchNextInputEvent() => _queue.DispatchNext();
+}
+
+internal class AutomaticRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue
+{
+    private readonly Queue<(RawInputEventArgs args, Action<RawInputEventArgs> handler)> _inputQueue = new();
     private readonly Action _dispatchFromQueue;
-    private readonly Dictionary<long, RawPointerEventArgs> _lastTouchPoints = new();
-    private RawInputEventArgs? _lastEvent;
 
-    public RawEventGrouper(Action<RawInputEventArgs> eventCallback)
+    public AutomaticRawEventGrouperDispatchQueue()
     {
-        _eventCallback = eventCallback;
         _dispatchFromQueue = DispatchFromQueue;
     }
     
-    private void AddToQueue(RawInputEventArgs args)
+    public void Add(RawInputEventArgs args, Action<RawInputEventArgs> handler)
     {
-        _lastEvent = args;
-        _inputQueue.Enqueue(args);
+        _inputQueue.Enqueue((args, handler));
+        
         if (_inputQueue.Count == 1)
             Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
+        
     }
-
+    
     private void DispatchFromQueue()
     {
         while (true)
@@ -43,17 +76,8 @@ internal class RawEventGrouper : IDisposable
 
             var ev = _inputQueue.Dequeue();
 
-            if (_lastEvent == ev) 
-                _lastEvent = null;
+            ev.handler(ev.args);
             
-            if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate)
-                _lastTouchPoints.Remove(touchUpdate.RawPointerId);
-
-            _eventCallback?.Invoke(ev);
-
-            if (ev is RawPointerEventArgs { IntermediatePoints.Value: PooledList<RawPointerPoint> list }) 
-                list.Dispose();
-
             if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1))
             {
                 Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
@@ -61,6 +85,47 @@ internal class RawEventGrouper : IDisposable
             }
         }
     }
+}
+
+internal class RawEventGrouper : IDisposable
+{
+    private readonly Action<RawInputEventArgs> _eventCallback;
+    private readonly IRawEventGrouperDispatchQueue _queue;
+    private readonly Dictionary<long, RawPointerEventArgs> _lastTouchPoints = new();
+    private RawInputEventArgs? _lastEvent;
+    private Action<RawInputEventArgs> _dispatch;
+    private bool _disposed;
+
+    public RawEventGrouper(Action<RawInputEventArgs> eventCallback, IRawEventGrouperDispatchQueue? queue = null)
+    {
+        _eventCallback = eventCallback;
+        _queue = queue ?? new AutomaticRawEventGrouperDispatchQueue();
+        _dispatch = Dispatch;
+    }
+    
+    private void AddToQueue(RawInputEventArgs args)
+    {
+        _lastEvent = args;
+        _queue.Add(args, _dispatch);
+    }
+
+    private void Dispatch(RawInputEventArgs ev)
+    {
+        if (!_disposed)
+        {
+            if (_lastEvent == ev)
+                _lastEvent = null;
+
+            if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate)
+                _lastTouchPoints.Remove(touchUpdate.RawPointerId);
+            
+            _eventCallback?.Invoke(ev);
+        }
+
+        if (ev is RawPointerEventArgs { IntermediatePoints.Value: PooledList<RawPointerPoint> list }) 
+            list.Dispose();
+    }
+
     
     public void HandleEvent(RawInputEventArgs args)
     {
@@ -123,7 +188,7 @@ internal class RawEventGrouper : IDisposable
 
     public void Dispose()
     {
-        _inputQueue.Clear();
+        _disposed = true;
         _lastEvent = null;
         _lastTouchPoints.Clear();
     }

+ 43 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1784,6 +1784,49 @@ namespace Avalonia.Win32.Interop
             return result;
         }
 
+        [Flags]
+        internal enum QueueStatusFlags
+        {
+            QS_KEY = 0x0001,
+            QS_MOUSEMOVE = 0x0002,
+            QS_MOUSEBUTTON = 0x0004,
+            QS_POSTMESSAGE = 0x0008,
+            QS_TIMER = 0x0010,
+            QS_PAINT = 0x0020,
+            QS_SENDMESSAGE = 0x0040,
+            QS_HOTKEY = 0x0080,
+            QS_ALLPOSTMESSAGE = 0x0100,
+            QS_EVENT = 0x02000,
+            QS_MOUSE = QS_MOUSEMOVE | QS_MOUSEBUTTON,
+            QS_INPUT = QS_MOUSE | QS_KEY,
+            QS_ALLEVENTS = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY,
+            QS_ALLINPUT = QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE
+        }
+
+        [Flags]
+        internal enum MsgWaitForMultipleObjectsFlags
+        {
+            MWMO_WAITALL = 0x0001,
+            MWMO_ALERTABLE = 0x0002,
+            MWMO_INPUTAVAILABLE = 0x0004
+        }
+
+        [DllImport("user32", EntryPoint="MsgWaitForMultipleObjectsEx", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto)]
+        private static extern int IntMsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds,
+            QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags);
+
+        internal static int MsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds, 
+            QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags)
+        {
+            int result = IntMsgWaitForMultipleObjectsEx(nCount, pHandles, dwMilliseconds, dwWakeMask, dwFlags);
+            if(result == -1)
+            {
+                throw new Win32Exception();
+            }
+
+            return result;
+        }
+
         [DllImport("user32.dll")]
         internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
 

+ 121 - 0
src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs

@@ -0,0 +1,121 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Avalonia.Threading;
+using Avalonia.Win32.Interop;
+using static Avalonia.Win32.Interop.UnmanagedMethods;
+namespace Avalonia.Win32;
+
+internal class Win32DispatcherImpl : IControlledDispatcherImpl
+{
+    private readonly IntPtr _messageWindow;
+    private static Thread? s_uiThread;
+    private readonly Stopwatch _clock = Stopwatch.StartNew();
+    public Win32DispatcherImpl(IntPtr messageWindow)
+    {
+        _messageWindow = messageWindow;
+        s_uiThread = Thread.CurrentThread;
+    }
+    
+    public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread;
+    internal const int SignalW = unchecked((int)0xdeadbeaf);
+    internal const int SignalL = unchecked((int)0x12345678);
+
+    public void Signal() =>
+        // Messages from PostMessage are always processed before any user input,
+        // so Win32 should call us ASAP
+        PostMessage(
+            _messageWindow,
+            (int)WindowsMessage.WM_DISPATCH_WORK_ITEM,
+            new IntPtr(SignalW),
+            new IntPtr(SignalL));
+    
+    public void DispatchWorkItem() => Signaled?.Invoke();
+    
+    public event Action? Signaled;
+    public event Action? Timer;
+
+    public void FireTimer() => Timer?.Invoke();
+
+    public void UpdateTimer(long? dueTimeInMs)
+    {
+        if (dueTimeInMs == null)
+        {
+            KillTimer(_messageWindow, (IntPtr)Win32Platform.TIMERID_DISPATCHER);
+        }
+        else
+        {
+            var interval = (uint)Math.Min(int.MaxValue - 10, Math.Max(1, Now - dueTimeInMs.Value));
+            SetTimer(
+                _messageWindow,
+                (IntPtr)Win32Platform.TIMERID_DISPATCHER,
+                interval,
+                null!);
+        }
+    }
+
+    public bool CanQueryPendingInput => true;
+    
+    public bool HasPendingInput
+    {
+        get
+        {
+            // We need to know if there is any pending input in the Win32
+            // queue because we want to only process Avalon "background"
+            // items after Win32 input has been processed.
+            //
+            // Win32 provides the GetQueueStatus API -- but it has a major
+            // drawback: it only counts "new" input.  This means that
+            // sometimes it could return false, even if there really is input
+            // that needs to be processed.  This results in very hard to
+            // find bugs.
+            //
+            // Luckily, Win32 also provides the MsgWaitForMultipleObjectsEx
+            // API.  While more awkward to use, this API can return queue
+            // status information even if the input is "old".  The various
+            // flags we use are:
+            //
+            // QS_INPUT
+            // This represents any pending input - such as mouse moves, or
+            // key presses.  It also includes the new GenericInput messages.
+            //
+            // QS_EVENT
+            // This is actually a private flag that represents the various
+            // events that can be queued in Win32.  Some of these events
+            // can cause input, but Win32 doesn't include them in the
+            // QS_INPUT flag.  An example is WM_MOUSELEAVE.
+            //
+            // QS_POSTMESSAGE
+            // If there is already a message in the queue, we need to process
+            // it before we can process input.
+            //
+            // MWMO_INPUTAVAILABLE
+            // This flag indicates that any input (new or old) is to be
+            // reported.
+            //
+
+            return MsgWaitForMultipleObjectsEx(0, null, 0,
+                QueueStatusFlags.QS_INPUT | QueueStatusFlags.QS_EVENT | QueueStatusFlags.QS_POSTMESSAGE,
+                MsgWaitForMultipleObjectsFlags.MWMO_INPUTAVAILABLE) == 0;
+        }
+    }
+
+    public void RunLoop(CancellationToken cancellationToken)
+    {
+        var result = 0;
+        while (!cancellationToken.IsCancellationRequested 
+               && (result = GetMessage(out var msg, IntPtr.Zero, 0, 0)) > 0)
+        {
+            TranslateMessage(ref msg);
+            DispatchMessage(ref msg);
+        }
+        if (result < 0)
+        {
+            Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform)
+                ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(RunLoop), Marshal.GetLastWin32Error());
+        }
+    }
+
+    public long Now => _clock.ElapsedMilliseconds;
+}

+ 16 - 83
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -107,21 +107,22 @@ namespace Avalonia
 
 namespace Avalonia.Win32
 {
-    internal class Win32Platform : IPlatformThreadingInterface, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl
+    internal class Win32Platform : IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl
     {
         private static readonly Win32Platform s_instance = new();
-        private static Thread? s_uiThread;
         private static Win32PlatformOptions? s_options;
         private static Compositor? s_compositor;
+        internal const int TIMERID_DISPATCHER = 1;
 
         private WndProc? _wndProcDelegate;
         private IntPtr _hwnd;
-        private readonly List<Delegate> _delegates = new();
+        private Win32DispatcherImpl _dispatcher;
 
         public Win32Platform()
         {
             SetDpiAwareness();
             CreateMessageWindow();
+            _dispatcher = new Win32DispatcherImpl(_hwnd);
         }
 
         internal static Win32Platform Instance => s_instance;
@@ -157,7 +158,7 @@ namespace Avalonia.Win32
                 .Bind<ICursorFactory>().ToConstant(CursorFactory.Instance)
                 .Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
                 .Bind<IPlatformSettings>().ToSingleton<Win32PlatformSettings>()
-                .Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
+                .Bind<IDispatcherImpl>().ToConstant(s_instance._dispatcher)
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<IRenderTimer>().ToConstant(renderTimer)
                 .Bind<IWindowingPlatform>().ToConstant(s_instance)
@@ -174,8 +175,6 @@ namespace Avalonia.Win32
                 .Bind<IMountedVolumeInfoProvider>().ToConstant(new WindowsMountedVolumeInfoProvider())
                 .Bind<IPlatformLifetimeEventsImpl>().ToConstant(s_instance);
             
-            s_uiThread = Thread.CurrentThread;
-
             var platformGraphics = options.CustomPlatformGraphics
                                    ?? Win32GlManager.Initialize();
             
@@ -184,88 +183,16 @@ namespace Avalonia.Win32
             
             s_compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), platformGraphics);
         }
-
-        public bool HasMessages()
-        {
-            return PeekMessage(out _, IntPtr.Zero, 0, 0, 0);
-        }
-
-        public void ProcessMessage()
-        {
-            if (GetMessage(out var msg, IntPtr.Zero, 0, 0) > -1)
-            {
-                TranslateMessage(ref msg);
-                DispatchMessage(ref msg);
-            }
-            else
-            {
-                Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform)
-                    ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(ProcessMessage), Marshal.GetLastWin32Error());
-
-            }
-        }
-
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            var result = 0;
-            while (!cancellationToken.IsCancellationRequested 
-                && (result = GetMessage(out var msg, IntPtr.Zero, 0, 0)) > 0)
-            {
-                TranslateMessage(ref msg);
-                DispatchMessage(ref msg);
-            }
-            if (result < 0)
-            {
-                Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform)
-                    ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(RunLoop), Marshal.GetLastWin32Error());
-            }
-        }
-
-        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action callback)
-        {
-            TimerProc timerDelegate = (_, _, _, _) => callback();
-
-            IntPtr handle = SetTimer(
-                IntPtr.Zero,
-                IntPtr.Zero,
-                (uint)interval.TotalMilliseconds,
-                timerDelegate);
-
-            // Prevent timerDelegate being garbage collected.
-            _delegates.Add(timerDelegate);
-
-            return Disposable.Create(() =>
-            {
-                _delegates.Remove(timerDelegate);
-                KillTimer(IntPtr.Zero, handle);
-            });
-        }
-
-        private const int SignalW = unchecked((int)0xdeadbeaf);
-        private const int SignalL = unchecked((int)0x12345678);
-
-        public void Signal(DispatcherPriority prio)
-        {
-            PostMessage(
-                _hwnd,
-                (int)WindowsMessage.WM_DISPATCH_WORK_ITEM,
-                new IntPtr(SignalW),
-                new IntPtr(SignalL));
-        }
-
-        public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread;
-
-        public event Action<DispatcherPriority?>? Signaled;
-
+        
         public event EventHandler<ShutdownRequestedEventArgs>? ShutdownRequested;
 
         [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")]
         private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
         {
-            if (msg == (int)WindowsMessage.WM_DISPATCH_WORK_ITEM && wParam.ToInt64() == SignalW && lParam.ToInt64() == SignalL)
-            {
-                Signaled?.Invoke(null);
-            }
+            if (msg == (int)WindowsMessage.WM_DISPATCH_WORK_ITEM 
+                && wParam.ToInt64() == Win32DispatcherImpl.SignalW 
+                && lParam.ToInt64() == Win32DispatcherImpl.SignalL) 
+                _dispatcher?.DispatchWorkItem();
 
             if(msg == (uint)WindowsMessage.WM_QUERYENDSESSION)
             {
@@ -292,6 +219,12 @@ namespace Avalonia.Win32
                     win32PlatformSettings.OnColorValuesChanged();   
                 }
             }
+
+            if (msg == (uint)WindowsMessage.WM_TIMER)
+            {
+                if (wParam == (IntPtr)TIMERID_DISPATCHER)
+                    _dispatcher?.FireTimer();
+            }
             
             TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam);
 

+ 0 - 5
src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs

@@ -14,11 +14,6 @@ namespace Avalonia.iOS
         public bool CurrentThreadIsLoopThread => NSThread.Current.IsMainThread;
         
         public event Action<DispatcherPriority?> Signaled;
-        public void RunLoop(CancellationToken cancellationToken)
-        {
-            //Mobile platforms are using external main loop
-            throw new NotSupportedException(); 
-        }
         
         public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
             => NSTimer.CreateRepeatingScheduledTimer(interval, _ => tick());

+ 10 - 10
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -962,12 +962,12 @@ namespace Avalonia.Base.UnitTests
                 var currentThreadId = Thread.CurrentThread.ManagedThreadId;
                 var raised = 0;
 
-                var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
-                threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
+                var dispatcherMock = new Mock<IDispatcherImpl>();
+                dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
                     .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
 
                 var services = new TestServices(
-                    threadingInterface: threadingInterfaceMock.Object);
+                    dispatcherImpl: dispatcherMock.Object);
 
                 target.PropertyChanged += (s, e) =>
                 {
@@ -1000,12 +1000,12 @@ namespace Avalonia.Base.UnitTests
                 var currentThreadId = Thread.CurrentThread.ManagedThreadId;
                 var raised = 0;
 
-                var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
-                threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
+                var dispatcherMock = new Mock<IDispatcherImpl>();
+                dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
                     .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
 
                 var services = new TestServices(
-                    threadingInterface: threadingInterfaceMock.Object);
+                    dispatcherImpl: dispatcherMock.Object);
 
                 target.PropertyChanged += (s, e) =>
                 {
@@ -1038,12 +1038,12 @@ namespace Avalonia.Base.UnitTests
                 var currentThreadId = Thread.CurrentThread.ManagedThreadId;
                 var raised = 0;
 
-                var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
+                var threadingInterfaceMock = new Mock<IDispatcherImpl>();
                 threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
                     .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
 
                 var services = new TestServices(
-                    threadingInterface: threadingInterfaceMock.Object);
+                    dispatcherImpl: threadingInterfaceMock.Object);
 
                 target.PropertyChanged += (s, e) =>
                 {
@@ -1071,12 +1071,12 @@ namespace Avalonia.Base.UnitTests
             var source = new Subject<double>();
             var currentThreadId = Thread.CurrentThread.ManagedThreadId;
 
-            var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
+            var threadingInterfaceMock = new Mock<IDispatcherImpl>();
             threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
                 .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
 
             var services = new TestServices(
-                threadingInterface: threadingInterfaceMock.Object);
+                dispatcherImpl: threadingInterfaceMock.Object);
 
             using (UnitTestApplication.Start(services))
             {

+ 3 - 3
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@@ -530,12 +530,12 @@ namespace Avalonia.Base.UnitTests
                 var currentThreadId = Thread.CurrentThread.ManagedThreadId;
                 var raised = 0;
 
-                var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
-                threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
+                var dispatcherMock = new Mock<IDispatcherImpl>();
+                dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
                     .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
 
                 var services = new TestServices(
-                    threadingInterface: threadingInterfaceMock.Object);
+                    dispatcherImpl: dispatcherMock.Object);
 
                 target.PropertyChanged += (s, e) =>
                 {

+ 21 - 27
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs

@@ -11,12 +11,12 @@ namespace Avalonia.Base.UnitTests
 {
     public class AvaloniaObjectTests_Threading
     {
-        private ThreadingInterface _threading = new ThreadingInterface(true);
+        private TestDipatcherImpl _threading = new(true);
 
         [Fact]
         public void AvaloniaObject_Constructor_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new TestDipatcherImpl())))
             {
                 Assert.Throws<InvalidOperationException>(() => new Class1());
             }
@@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void StyledProperty_GetValue_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -36,7 +36,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void StyledProperty_SetValue_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -47,7 +47,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Setting_StyledProperty_Binding_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void StyledProperty_ClearValue_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -72,7 +72,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void StyledProperty_IsSet_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void DirectProperty_GetValue_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -94,7 +94,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void DirectProperty_SetValue_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -105,7 +105,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Setting_DirectProperty_Binding_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -119,7 +119,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void DirectProperty_ClearValue_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -130,7 +130,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void DirectProperty_IsSet_Should_Throw()
         {
-            using (UnitTestApplication.Start(new TestServices(threadingInterface: _threading)))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
             {
                 var target = new Class1();
                 _threading.CurrentThreadIsLoopThread = false;
@@ -147,10 +147,10 @@ namespace Avalonia.Base.UnitTests
                 AvaloniaProperty.RegisterDirect<Class1, string>("Qux", _ => null, (o, v) => { });
         }
 
-        private class ThreadingInterface : IPlatformThreadingInterface
+        private class TestDipatcherImpl : IDispatcherImpl
         {
 
-            public ThreadingInterface(bool isLoopThread = false)
+            public TestDipatcherImpl(bool isLoopThread = false)
             {
                 CurrentThreadIsLoopThread = isLoopThread;
             }
@@ -158,23 +158,17 @@ namespace Avalonia.Base.UnitTests
             public bool CurrentThreadIsLoopThread { get; set; }
 
 #pragma warning disable 67
-            public event Action<DispatcherPriority?> Signaled;
-#pragma warning restore 67
-
-            public void RunLoop(CancellationToken cancellationToken)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void Signal(DispatcherPriority prio)
+            public event Action Signaled;
+            public event Action Timer;
+            public long Now => 0;
+            public void UpdateTimer(long? dueTimeInMs)
             {
                 throw new NotImplementedException();
             }
+            public void Signal() => throw new NotImplementedException();
+#pragma warning restore 67
 
-            public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
-            {
-                throw new NotImplementedException();
-            }
+            
         }
     }
 }

+ 12 - 1
tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs

@@ -62,13 +62,24 @@ public class CompositionAnimationTests
             }
         }
     }
+
+    class DummyDispatcher : IDispatcher
+    {
+        public bool CheckAccess() => true;
+
+        public void VerifyAccess()
+        {
+        }
+
+        public void Post(Action action, DispatcherPriority priority = default) => throw new NotSupportedException();
+    }
     
     [AnimationDataProvider]
     [Theory]
     public void GenericCheck(AnimationData data)
     {
         var compositor =
-            new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer(), new Dispatcher(null)), null);
+            new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer(), new DummyDispatcher()), null);
         var target = compositor.CreateSolidColorVisual();
         var ani = new ScalarKeyFrameAnimation(null);
         foreach (var frame in data.Frames)

+ 173 - 0
tests/Avalonia.Base.UnitTests/DispatcherTests.cs

@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Threading;
+using Xunit;
+namespace Avalonia.Base.UnitTests;
+
+public class DispatcherTests
+{
+    class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput
+    {
+        public bool CurrentThreadIsLoopThread => true;
+
+        public void Signal() => AskedForSignal = true;
+
+        public event Action Signaled;
+        public event Action Timer;
+        public long? NextTimer { get; private set; }
+        public bool AskedForSignal { get; private set; }
+        
+        public void UpdateTimer(long? dueTimeInTicks)
+        {
+            NextTimer = dueTimeInTicks;
+        }
+
+        public long Now { get; set; }
+
+        public void ExecuteSignal()
+        {
+            if (!AskedForSignal)
+                return;
+            AskedForSignal = false;
+            Signaled?.Invoke();
+        }
+
+        public void ExecuteTimer()
+        {
+            if (NextTimer == null)
+                return;
+            Now = NextTimer.Value;
+            Timer?.Invoke();
+        }
+
+        public bool CanQueryPendingInput => TestInputPending != null;
+        public bool HasPendingInput => TestInputPending == true;
+        public bool? TestInputPending { get; set; }
+    }
+    
+    
+    [Fact]
+    public void DispatcherExecutesJobsAccordingToPriority()
+    {
+        var impl = new SimpleDispatcherImpl();
+        var disp = new Dispatcher(impl);
+        var actions = new List<string>();
+        disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background);
+        disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render);
+        disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input);
+        Assert.True(impl.AskedForSignal);
+        impl.ExecuteSignal();
+        Assert.Equal(new[] { "Render", "Input", "Background" }, actions);
+    }
+    
+    [Fact]
+    public void DispatcherPreservesOrderWhenChangingPriority()
+    {
+        var impl = new SimpleDispatcherImpl();
+        var disp = new Dispatcher(impl);
+        var actions = new List<string>();
+        var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background);
+        var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input);
+        disp.Post(() => actions.Add("Render"), DispatcherPriority.Render);
+        
+        toPromote.Priority = DispatcherPriority.Render;
+        toPromote2.Priority = DispatcherPriority.Render;
+        
+        Assert.True(impl.AskedForSignal);
+        impl.ExecuteSignal();
+        
+        Assert.Equal(new[] { "PromotedRender", "PromotedRender2", "Render" }, actions);
+    }
+
+    [Fact]
+    public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached()
+    {
+        var impl = new SimpleDispatcherImpl();
+        var disp = new Dispatcher(impl);
+        var actions = new List<int>();
+        for (var c = 0; c < 10; c++)
+        {
+            var itemId = c;
+            disp.Post(() =>
+            {
+                actions.Add(itemId);
+                impl.Now += 20;
+            }, DispatcherPriority.Background);
+        }
+
+        Assert.False(impl.AskedForSignal);
+        Assert.NotNull(impl.NextTimer);
+
+        for (var c = 0; c < 4; c++)
+        {
+            Assert.NotNull(impl.NextTimer);
+            Assert.False(impl.AskedForSignal);
+            impl.ExecuteTimer();
+            Assert.False(impl.AskedForSignal);
+            impl.ExecuteSignal();
+            var expectedCount = (c + 1) * 3;
+            if (c == 3)
+                expectedCount = 10;
+            
+            Assert.Equal(Enumerable.Range(0, expectedCount), actions);
+            Assert.False(impl.AskedForSignal);
+            if (c < 3)
+            {
+                Assert.True(impl.NextTimer > impl.Now);
+            }
+            else
+                Assert.Null(impl.NextTimer);
+        }
+    }
+    
+    
+    [Fact]
+    public void DispatcherStopsItemProcessingWhenInputIsPending()
+    {
+        var impl = new SimpleDispatcherImpl();
+        impl.TestInputPending = true;
+        var disp = new Dispatcher(impl);
+        var actions = new List<int>();
+        for (var c = 0; c < 10; c++)
+        {
+            var itemId = c;
+            disp.Post(() =>
+            {
+                actions.Add(itemId);
+                if (itemId == 0 || itemId == 3 || itemId == 7)
+                    impl.TestInputPending = true;
+            }, DispatcherPriority.Background);
+        }
+        Assert.False(impl.AskedForSignal);
+        Assert.NotNull(impl.NextTimer);
+        impl.TestInputPending = false;
+
+        for (var c = 0; c < 4; c++)
+        {
+            Assert.NotNull(impl.NextTimer);
+            impl.ExecuteTimer();
+            Assert.False(impl.AskedForSignal);
+            var expectedCount = c switch
+            {
+                0 => 1,
+                1 => 4,
+                2 => 8,
+                3 => 10
+            };
+            
+            Assert.Equal(Enumerable.Range(0, expectedCount), actions);
+            Assert.False(impl.AskedForSignal);
+            if (c < 3)
+            {
+                Assert.True(impl.NextTimer > impl.Now);
+                impl.Now = impl.NextTimer.Value + 1;
+            }
+            else
+                Assert.Null(impl.NextTimer);
+
+            impl.TestInputPending = false;
+        }
+    }
+    
+}

+ 55 - 73
tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs

@@ -1,5 +1,7 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Base.UnitTests.Utilities;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Input.GestureRecognizers;
@@ -7,14 +9,16 @@ using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Threading;
 using Avalonia.UnitTests;
+using Avalonia.Utilities;
 using Moq;
 using Xunit;
+// ReSharper disable RedundantArgumentDefaultValue
 
 namespace Avalonia.Base.UnitTests.Input
 {
     public class GesturesTests
     {
-        private MouseTestHelper _mouse = new MouseTestHelper();
+        private readonly MouseTestHelper _mouse = new MouseTestHelper();
         
         [Fact]
         public void Tapped_Should_Follow_Pointer_Pressed_Released()
@@ -60,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.TappedEvent, (_, _) => raised = true);
 
             _mouse.Click(border, MouseButton.Middle);
 
@@ -77,7 +81,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.TappedEvent, (_, _) => raised = true);
 
             _mouse.Click(border, MouseButton.Right);
 
@@ -94,7 +98,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.RightTappedEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.RightTappedEvent, (_, _) => raised = true);
 
             _mouse.Click(border, MouseButton.Right);
 
@@ -147,7 +151,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.DoubleTappedEvent, (_, _) => raised = true);
 
             _mouse.Click(border, MouseButton.Middle);
             _mouse.Down(border, MouseButton.Middle, clickCount: 2);
@@ -165,7 +169,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.DoubleTappedEvent, (_, _) => raised = true);
 
             _mouse.Click(border, MouseButton.Right);
             _mouse.Down(border, MouseButton.Right, clickCount: 2);
@@ -182,10 +186,8 @@ namespace Avalonia.Base.UnitTests.Input
             iSettingsMock.Setup(x => x.GetTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
-
-            var scheduledTimers = new List<(TimeSpan time, Action action)>();
-            using var app = UnitTestApplication.Start(new TestServices(
-                threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
+            
+            using var app = UnitTestApplication.Start();
 
             Border border = new Border();
             Gestures.SetIsHoldWithMouseEnabled(border, true);
@@ -195,15 +197,15 @@ namespace Avalonia.Base.UnitTests.Input
             };
             HoldingState holding = HoldingState.Cancelled;
 
-            decorator.AddHandler(Gestures.HoldingEvent, (s, e) => holding = e.HoldingState);
+            decorator.AddHandler(Gestures.HoldingEvent, (_, e) => holding = e.HoldingState);
             
             _mouse.Down(border);
             Assert.False(holding != HoldingState.Cancelled);
             
             // Verify timer duration, but execute it immediately.
-            var timer = Assert.Single(scheduledTimers);
-            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
-            timer.action();
+            var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval);
+            timer.ForceFire();
 
             Assert.True(holding == HoldingState.Started);
 
@@ -220,10 +222,8 @@ namespace Avalonia.Base.UnitTests.Input
             iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
-
-            var scheduledTimers = new List<(TimeSpan time, Action action)>();
-            using var app = UnitTestApplication.Start(new TestServices(
-                threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
+            
+            using var app = UnitTestApplication.Start();
 
             Border border = new Border();
             Gestures.SetIsHoldWithMouseEnabled(border, true);
@@ -233,7 +233,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Started);
+            decorator.AddHandler(Gestures.HoldingEvent, (_, e) => raised = e.HoldingState == HoldingState.Started);
             
             _mouse.Down(border);
             Assert.False(raised);
@@ -242,9 +242,9 @@ namespace Avalonia.Base.UnitTests.Input
             Assert.False(raised);
             
             // Verify timer duration, but execute it immediately.
-            var timer = Assert.Single(scheduledTimers);
-            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
-            timer.action();
+            var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval);
+            timer.ForceFire();
 
             Assert.False(raised);
         }
@@ -257,10 +257,8 @@ namespace Avalonia.Base.UnitTests.Input
             iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
-
-            var scheduledTimers = new List<(TimeSpan time, Action action)>();
-            using var app = UnitTestApplication.Start(new TestServices(
-                threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
+            
+            using var app = UnitTestApplication.Start();
 
             Border border = new Border();
             Gestures.SetIsHoldWithMouseEnabled(border, true);
@@ -270,7 +268,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed);
+            decorator.AddHandler(Gestures.HoldingEvent, (_, e) => raised = e.HoldingState == HoldingState.Completed);
             
             _mouse.Down(border);
             Assert.False(raised);
@@ -279,9 +277,9 @@ namespace Avalonia.Base.UnitTests.Input
             Assert.False(raised);
             
             // Verify timer duration, but execute it immediately.
-            var timer = Assert.Single(scheduledTimers);
-            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
-            timer.action();
+            var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval);
+            timer.ForceFire();
 
             Assert.False(raised);
         }
@@ -294,10 +292,8 @@ namespace Avalonia.Base.UnitTests.Input
             iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
-
-            var scheduledTimers = new List<(TimeSpan time, Action action)>();
-            using var app = UnitTestApplication.Start(new TestServices(
-                threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
+            
+            using var app = UnitTestApplication.Start();
 
             Border border = new Border();
             Gestures.SetIsHoldWithMouseEnabled(border, true);
@@ -307,14 +303,14 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var cancelled = false;
 
-            decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
+            decorator.AddHandler(Gestures.HoldingEvent, (_, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
             
             _mouse.Down(border);
             Assert.False(cancelled);
 
-            var timer = Assert.Single(scheduledTimers);
-            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
-            timer.action();
+            var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval);
+            timer.ForceFire();
 
             var secondMouse = new MouseTestHelper();
 
@@ -333,9 +329,7 @@ namespace Avalonia.Base.UnitTests.Input
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
 
-            var scheduledTimers = new List<(TimeSpan time, Action action)>();
-            using var app = UnitTestApplication.Start(new TestServices(
-                threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
+            using var app = UnitTestApplication.Start();
 
             Border border = new Border();
             Gestures.SetIsHoldWithMouseEnabled(border, true);
@@ -345,13 +339,13 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var cancelled = false;
 
-            decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
+            decorator.AddHandler(Gestures.HoldingEvent, (_, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
             
             _mouse.Down(border);
 
-            var timer = Assert.Single(scheduledTimers);
-            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
-            timer.action();
+            var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval);
+            timer.ForceFire();
 
             _mouse.Move(border, position: new Point(3, 3));
 
@@ -371,9 +365,7 @@ namespace Avalonia.Base.UnitTests.Input
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
 
-            var scheduledTimers = new List<(TimeSpan time, Action action)>();
-            using var app = UnitTestApplication.Start(new TestServices(
-                threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
+            using var app = UnitTestApplication.Start();
             
             Border border = new Border();
             Gestures.SetIsHoldWithMouseEnabled(border, true);
@@ -383,31 +375,21 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed);
+            decorator.AddHandler(Gestures.HoldingEvent, (_, e) => raised = e.HoldingState == HoldingState.Completed);
 
             var secondMouse = new MouseTestHelper();
 
             _mouse.Down(border, MouseButton.Left);
 
             // Verify timer duration, but execute it immediately.
-            var timer = Assert.Single(scheduledTimers);
-            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
-            timer.action();
+            var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+            Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.Interval);
+            timer.ForceFire();
 
             secondMouse.Down(border, MouseButton.Left);
 
             Assert.False(raised);
         }
-
-        private static IPlatformThreadingInterface CreatePlatformThreadingInterface(Action<(TimeSpan, Action)> callback)
-        {
-            var threadingInterface = new Mock<IPlatformThreadingInterface>();
-            threadingInterface.SetupGet(p => p.CurrentThreadIsLoopThread).Returns(true);
-            threadingInterface.Setup(p => p
-                    .StartTimer(It.IsAny<DispatcherPriority>(), It.IsAny<TimeSpan>(), It.IsAny<Action>()))
-                .Callback<DispatcherPriority, TimeSpan, Action>((_, t, a) => callback((t, a)));
-            return threadingInterface.Object;
-        }
         
         private static void AddHandlers(
             Decorator decorator,
@@ -415,7 +397,7 @@ namespace Avalonia.Base.UnitTests.Input
             IList<string> result,
             bool markHandled)
         {
-            decorator.AddHandler(Border.PointerPressedEvent, (s, e) =>
+            decorator.AddHandler(InputElement.PointerPressedEvent, (_, e) =>
             {
                 result.Add("dp");
 
@@ -425,7 +407,7 @@ namespace Avalonia.Base.UnitTests.Input
                 }
             });
 
-            decorator.AddHandler(Border.PointerReleasedEvent, (s, e) =>
+            decorator.AddHandler(InputElement.PointerReleasedEvent, (_, e) =>
             {
                 result.Add("dr");
 
@@ -435,13 +417,13 @@ namespace Avalonia.Base.UnitTests.Input
                 }
             });
 
-            border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp"));
-            border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br"));
+            border.AddHandler(InputElement.PointerPressedEvent, (_, _) => result.Add("bp"));
+            border.AddHandler(InputElement.PointerReleasedEvent, (_, _) => result.Add("br"));
 
-            decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
-            decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt"));
-            border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
-            border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
+            decorator.AddHandler(Gestures.TappedEvent, (_, _) => result.Add("dt"));
+            decorator.AddHandler(Gestures.DoubleTappedEvent, (_, _) => result.Add("ddt"));
+            border.AddHandler(Gestures.TappedEvent, (_, _) => result.Add("bt"));
+            border.AddHandler(Gestures.DoubleTappedEvent, (_, _) => result.Add("bdt"));
         }
 
         [Fact]
@@ -462,7 +444,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.PinchEvent, (_, _) => raised = true);
 
             var firstPoint = new Point(5, 5);
             var secondPoint = new Point(10, 10);
@@ -490,7 +472,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.PinchEvent, (_, _) => raised = true);
 
             var firstPoint = new Point(5, 5);
             var secondPoint = new Point(10, 10);
@@ -526,7 +508,7 @@ namespace Avalonia.Base.UnitTests.Input
             };
             var raised = false;
 
-            decorator.AddHandler(Gestures.ScrollGestureEvent, (s, e) => raised = true);
+            decorator.AddHandler(Gestures.ScrollGestureEvent, (_, _) => raised = true);
 
             var firstTouch = new TouchTestHelper();
 

+ 4 - 3
tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs

@@ -3,6 +3,7 @@ using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -16,9 +17,9 @@ namespace Avalonia.Base.UnitTests.Input
         {
             using var scope = AvaloniaLocator.EnterScope();
             var settingsMock = new Mock<IPlatformSettings>();
-            var threadingMock = new Mock<IPlatformThreadingInterface>();
+            var dispatcherMock = new Mock<IDispatcherImpl>();
 
-            threadingMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true);
+            dispatcherMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true);
 
             AvaloniaLocator.CurrentMutable.BindToSelf(this)
                 .Bind<IPlatformSettings>().ToConstant(settingsMock.Object);
@@ -26,7 +27,7 @@ namespace Avalonia.Base.UnitTests.Input
             using var app = UnitTestApplication.Start(
                 new TestServices(
                     inputManager: new InputManager(),
-                    threadingInterface: threadingMock.Object));
+                    dispatcherImpl: dispatcherMock.Object));
 
             var renderer = RendererMocks.CreateRenderer();
             var device = new MouseDevice();

+ 2 - 1
tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

@@ -1,6 +1,7 @@
 using System;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -207,7 +208,7 @@ namespace Avalonia.Input.UnitTests
         private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan())
         {
             var unitTestApp = UnitTestApplication.Start(
-                new TestServices(inputManager: new InputManager(), threadingInterface: Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true)));
+                new TestServices(inputManager: new InputManager(), dispatcherImpl: Mock.Of<IDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true)));
             var iSettingsMock = new Mock<IPlatformSettings>();
             iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny<PointerType>())).Returns(doubleClickTime);
             iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));

+ 2 - 3
tests/Avalonia.Base.UnitTests/Rendering/RenderLoopTests.cs

@@ -47,9 +47,8 @@ namespace Avalonia.Base.UnitTests.Rendering
         {
             var dispatcher = new Mock<IDispatcher>();
             dispatcher.Setup(
-                d => d.InvokeAsync(It.IsAny<Action>(), DispatcherPriority.Render))
-                .Callback((Action a, DispatcherPriority _) => a())
-                .Returns(Task.CompletedTask);
+                    d => d.Post(It.IsAny<Action>(), DispatcherPriority.Render))
+                .Callback((Action a, DispatcherPriority _) => a());
 
             var timer = new Mock<IRenderTimer>();
             var loop = new RenderLoop(timer.Object, dispatcher.Object);

+ 1 - 1
tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Benchmarks.Layout
             _app = UnitTestApplication.Start(
                 TestServices.StyledWindow.With(
                     renderInterface: new NullRenderingPlatform(),
-                    threadingInterface: new NullThreadingPlatform(),
+                    dispatcherImpl: new NullThreadingPlatform(),
                     standardCursorFactory: new NullCursorFactory()));
 
             _root = new TestRoot(true, null)

+ 7 - 11
tests/Avalonia.Benchmarks/NullThreadingPlatform.cs

@@ -6,26 +6,22 @@ using Avalonia.Threading;
 
 namespace Avalonia.Benchmarks
 {
-    internal class NullThreadingPlatform : IPlatformThreadingInterface
+    internal class NullThreadingPlatform : IDispatcherImpl
     {
-        public void RunLoop(CancellationToken cancellationToken)
+        public void Signal()
         {
         }
-
-        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
-        {
-            return Disposable.Empty;
-        }
-
-        public void Signal(DispatcherPriority priority)
+        
+        public void UpdateTimer(long? dueTimeInMs)
         {
         }
 
         public bool CurrentThreadIsLoopThread => true;
 
 #pragma warning disable CS0067
-        public event Action<DispatcherPriority?> Signaled;
+        public event Action Signaled;
+        public event Action Timer;
+        public long Now => 0;
 #pragma warning restore CS0067
-
     }
 }

+ 1 - 1
tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Benchmarks.Styling
             _app = UnitTestApplication.Start(
                 TestServices.StyledWindow.With(
                     renderInterface: new NullRenderingPlatform(),
-                    threadingInterface: new NullThreadingPlatform()));
+                    dispatcherImpl: new NullThreadingPlatform()));
 
             // Simulate an application with a lot of styles by creating a tree of nested panels,
             // each with a bunch of styles applied.

+ 1 - 1
tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Benchmarks.Styling
                 renderInterface: new MockPlatformRenderInterface(),
                 standardCursorFactory: Mock.Of<ICursorFactory>(),
                 theme: () => CreateTheme(),
-                threadingInterface: new NullThreadingPlatform(),
+                dispatcherImpl: new NullThreadingPlatform(),
                 fontManagerImpl: new MockFontManagerImpl(),
                 textShaperImpl: new MockTextShaperImpl(),
                 windowingPlatform: new MockWindowingPlatform());

+ 1 - 1
tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Benchmarks.Styling
             _app = UnitTestApplication.Start(
                 TestServices.StyledWindow.With(
                     renderInterface: new NullRenderingPlatform(),
-                    threadingInterface: new NullThreadingPlatform()));
+                    dispatcherImpl: new NullThreadingPlatform()));
 
             // Simulate an application with a lot of styles by creating a tree of nested panels,
             // each with a bunch of styles applied.

+ 1 - 1
tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

@@ -32,7 +32,7 @@ public class HugeTextLayout : IDisposable
 
         var testServices = TestServices.StyledWindow.With(
             renderInterface: new NullRenderingPlatform(),
-            threadingInterface: new NullThreadingPlatform(),
+            dispatcherImpl: new NullThreadingPlatform(),
             standardCursorFactory: new NullCursorFactory());
 
         if (s_useSkia)

+ 1 - 1
tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs

@@ -47,7 +47,7 @@ namespace Avalonia.Benchmarks.Themes
                 renderInterface: new MockPlatformRenderInterface(),
                 standardCursorFactory: Mock.Of<ICursorFactory>(),
                 theme: () => LoadFluentTheme(),
-                threadingInterface: new NullThreadingPlatform(),
+                dispatcherImpl: new NullThreadingPlatform(),
                 fontManagerImpl: new MockFontManagerImpl(),
                 textShaperImpl: new MockTextShaperImpl(),
                 windowingPlatform: new MockWindowingPlatform());

+ 6 - 2
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -12,10 +13,13 @@ namespace Avalonia.Controls.UnitTests
     
     public class DesktopStyleApplicationLifetimeTests
     {
+        IDispatcherImpl CreateDispatcherWithInstantMainLoop() => Mock.Of<IControlledDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true);
+
+
         [Fact]
         public void Should_Set_ExitCode_After_Shutdown()
         {
-            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            using (UnitTestApplication.Start(new TestServices(dispatcherImpl: CreateDispatcherWithInstantMainLoop())))
             using(var lifetime = new ClassicDesktopStyleApplicationLifetime())    
             {
                 lifetime.Shutdown(1337);
@@ -215,7 +219,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event()
         {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: CreateDispatcherWithInstantMainLoop())))
             using (var lifetime = new ClassicDesktopStyleApplicationLifetime())
             {
                 var lifetimeEvents = new Mock<IPlatformLifetimeEventsImpl>();

+ 3 - 7
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@@ -2084,13 +2084,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
         [Fact]
         public void Setting_IsTextSearchEnabled_Enables_Or_Disables_Text_Search()
         {
-            var pti = Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true);
-
-            Mock.Get(pti)
-                .Setup(v => v.StartTimer(It.IsAny<DispatcherPriority>(), It.IsAny<TimeSpan>(), It.IsAny<Action>()))
-                .Returns(Disposable.Empty);
-
-            using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti)))
+            var pti = Mock.Of<IDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true);
+            
+            using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: pti)))
             {
                 var items = new[]
                 {

+ 5 - 18
tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

@@ -4,6 +4,7 @@ using Avalonia.Markup.Xaml;
 using Avalonia.Platform;
 using Avalonia.Threading;
 using Avalonia.UnitTests;
+using Avalonia.Utilities;
 using Avalonia.VisualTree;
 using Moq;
 using Xunit;
@@ -160,21 +161,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Should_Open_On_Pointer_Enter_With_Delay()
         {
-            Action timercallback = null;
-            var delay = TimeSpan.Zero;
-
-            var pti = Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true);
-
-            Mock.Get(pti)
-                .Setup(v => v.StartTimer(It.IsAny<DispatcherPriority>(), It.IsAny<TimeSpan>(), It.IsAny<Action>()))
-                .Callback<DispatcherPriority, TimeSpan, Action>((priority, interval, tick) =>
-                {
-                    delay = interval;
-                    timercallback = tick;
-                })
-                .Returns(Disposable.Empty);
-
-            using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti)))
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var window = new Window();
 
@@ -194,11 +181,11 @@ namespace Avalonia.Controls.UnitTests
 
                 _mouseHelper.Enter(target);
 
-                Assert.Equal(TimeSpan.FromMilliseconds(1), delay);
-                Assert.NotNull(timercallback);
+                var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+                Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Interval);
                 Assert.False(ToolTip.GetIsOpen(target));
 
-                timercallback();
+                timer.ForceFire();
 
                 Assert.True(ToolTip.GetIsOpen(target));
             }

+ 1 - 0
tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj

@@ -16,6 +16,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
     <ProjectReference Include="..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj" />
+    <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
   </ItemGroup>
   <Import Project="..\..\build\Moq.props" />
   <Import Project="..\..\build\Rx.props" />

+ 1 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
+using System.Runtime.CompilerServices;
 using System.Xml;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml.Styling;

+ 2 - 0
tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Reflection;
 using System.Text;
+using Avalonia.Controls;
 using Avalonia.Data;
 
 namespace Avalonia.Markup.Xaml.UnitTests
@@ -13,6 +14,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
         {
             // Ensure necessary assemblies are loaded.
             var _ = typeof(TemplateBinding);
+            GC.KeepAlive(typeof(ItemsRepeater));
             if (AvaloniaLocator.Current.GetService<AvaloniaXamlLoader.IRuntimeXamlLoader>() == null)
                 AvaloniaLocator.CurrentMutable.Bind<AvaloniaXamlLoader.IRuntimeXamlLoader>()
                     .ToConstant(new TestXamlLoaderShim());

+ 14 - 29
tests/Avalonia.RenderTests/TestBase.cs

@@ -17,6 +17,8 @@ using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Media;
 using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Avalonia.Utilities;
 using SixLabors.ImageSharp.PixelFormats;
 using Image = SixLabors.ImageSharp.Image;
 #if AVALONIA_SKIA
@@ -40,8 +42,8 @@ namespace Avalonia.Direct2D1.RenderTests
 #endif
         public static FontFamily TestFontFamily = new FontFamily(s_fontUri);
 
-        private static readonly TestThreadingInterface threadingInterface =
-            new TestThreadingInterface();
+        private static readonly TestDispatcherImpl threadingInterface =
+            new TestDispatcherImpl();
 
         private static readonly IAssetLoader assetLoader = new AssetLoader();
         
@@ -53,7 +55,7 @@ namespace Avalonia.Direct2D1.RenderTests
             Direct2D1Platform.Initialize();
 #endif
             AvaloniaLocator.CurrentMutable
-                .Bind<IPlatformThreadingInterface>()
+                .Bind<IDispatcherImpl>()
                 .ToConstant(threadingInterface);
 
             AvaloniaLocator.CurrentMutable
@@ -122,7 +124,8 @@ namespace Avalonia.Direct2D1.RenderTests
 
                 // Free pools
                 for (var c = 0; c < 11; c++)
-                    TestThreadingInterface.RunTimers();
+                    foreach (var dp in Dispatcher.SnapshotTimersForUnitTests())
+                        dp.ForceFire();
                 writableBitmap.Save(compositedPath);
             }
         }
@@ -241,45 +244,27 @@ namespace Avalonia.Direct2D1.RenderTests
             return path;
         }
 
-        private class TestThreadingInterface : IPlatformThreadingInterface
+        private class TestDispatcherImpl : IDispatcherImpl
         {
             public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId;
 
             public Thread MainThread { get; set; }
 
 #pragma warning disable 67
-            public event Action<DispatcherPriority?> Signaled;
+            public event Action Signaled;
+            public event Action Timer;
 #pragma warning restore 67
 
-            public void RunLoop(CancellationToken cancellationToken)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void Signal(DispatcherPriority prio)
+            public void Signal()
             {
                 // No-op
             }
 
-            private static List<Action> s_timers = new();
-            
-            public static void RunTimers()
-            {
-                lock (s_timers)
-                {
-                    foreach(var t in s_timers.ToList())
-                        t.Invoke();
-                }
-            }
+            public long Now => 0;
 
-            public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
+            public void UpdateTimer(long? dueTimeInMs)
             {
-                var act = () => tick();
-                lock (s_timers) s_timers.Add(act);
-                return Disposable.Create(() =>
-                {
-                    lock (s_timers) s_timers.Remove(act);
-                });
+                // No-op
             }
         }
 

+ 1 - 0
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@@ -17,6 +17,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
     <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
+    <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
   </ItemGroup>
   <Import Project="..\..\build\Moq.props" />
   <Import Project="..\..\build\Rx.props" />

+ 13 - 0
tests/Avalonia.UnitTests/DispatcherTimerHelper.cs

@@ -0,0 +1,13 @@
+using Avalonia.Threading;
+
+namespace Avalonia.UnitTests;
+
+public static class DispatcherTimerUtils
+{
+    public static void ForceFire(this DispatcherTimer timer)
+    {
+        timer.Promote();
+        timer.Dispatcher.RemoveTimer(timer);
+        timer.Dispatcher.RunJobs();
+    }
+}

+ 8 - 7
tests/Avalonia.UnitTests/TestServices.cs

@@ -13,6 +13,7 @@ using System.Collections.Generic;
 using Avalonia.Controls;
 using System.Reflection;
 using Avalonia.Animation;
+using Avalonia.Threading;
 
 namespace Avalonia.UnitTests
 {
@@ -24,7 +25,7 @@ namespace Avalonia.UnitTests
             renderInterface: new MockPlatformRenderInterface(),
             standardCursorFactory: Mock.Of<ICursorFactory>(),
             theme: () => CreateSimpleTheme(),
-            threadingInterface: Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true),
+            dispatcherImpl: Mock.Of<IDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true),
             fontManagerImpl: new MockFontManagerImpl(),
             textShaperImpl: new MockTextShaperImpl(),
             windowingPlatform: new MockWindowingPlatform());
@@ -39,7 +40,7 @@ namespace Avalonia.UnitTests
             platform: Mock.Of<IRuntimePlatform>());
 
         public static readonly TestServices MockThreadingInterface = new TestServices(
-            threadingInterface: Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true));
+            dispatcherImpl: Mock.Of<IDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true));
 
         public static readonly TestServices MockWindowingPlatform = new TestServices(
             windowingPlatform: new MockWindowingPlatform());
@@ -73,7 +74,7 @@ namespace Avalonia.UnitTests
             IRenderTimer renderLoop = null,
             ICursorFactory standardCursorFactory = null,
             Func<IStyle> theme = null,
-            IPlatformThreadingInterface threadingInterface = null,
+            IDispatcherImpl dispatcherImpl = null,
             IFontManagerImpl fontManagerImpl = null,
             ITextShaperImpl textShaperImpl = null,
             IWindowImpl windowImpl = null,
@@ -92,7 +93,7 @@ namespace Avalonia.UnitTests
             TextShaperImpl = textShaperImpl;
             StandardCursorFactory = standardCursorFactory;
             Theme = theme;
-            ThreadingInterface = threadingInterface;
+            DispatcherImpl = dispatcherImpl;
             WindowImpl = windowImpl;
             WindowingPlatform = windowingPlatform;
         }
@@ -110,7 +111,7 @@ namespace Avalonia.UnitTests
         public ITextShaperImpl TextShaperImpl { get; }
         public ICursorFactory StandardCursorFactory { get; }
         public Func<IStyle> Theme { get; }
-        public IPlatformThreadingInterface ThreadingInterface { get; }
+        public IDispatcherImpl DispatcherImpl { get; }
         public IWindowImpl WindowImpl { get; }
         public IWindowingPlatform WindowingPlatform { get; }
 
@@ -128,7 +129,7 @@ namespace Avalonia.UnitTests
             IScheduler scheduler = null,
             ICursorFactory standardCursorFactory = null,
             Func<IStyle> theme = null,
-            IPlatformThreadingInterface threadingInterface = null,
+            IDispatcherImpl dispatcherImpl = null,
             IFontManagerImpl fontManagerImpl = null,
             ITextShaperImpl textShaperImpl = null,
             IWindowImpl windowImpl = null,
@@ -148,7 +149,7 @@ namespace Avalonia.UnitTests
                 textShaperImpl: textShaperImpl ?? TextShaperImpl,
                 standardCursorFactory: standardCursorFactory ?? StandardCursorFactory,
                 theme: theme ?? Theme,
-                threadingInterface: threadingInterface ?? ThreadingInterface,
+                dispatcherImpl: dispatcherImpl ?? DispatcherImpl,
                 windowingPlatform: windowingPlatform ?? WindowingPlatform,
                 windowImpl: windowImpl ?? WindowImpl);
         }

+ 3 - 3
tests/Avalonia.UnitTests/UnitTestApplication.cs

@@ -43,7 +43,7 @@ namespace Avalonia.UnitTests
         {
             var scope = AvaloniaLocator.EnterScope();
             var app = new UnitTestApplication(services);
-            Dispatcher.UIThread.UpdateServices();
+            Dispatcher.ResetForUnitTests();
             return Disposable.Create(() =>
             {
                 if (Dispatcher.UIThread.CheckAccess())
@@ -52,7 +52,7 @@ namespace Avalonia.UnitTests
                 }
 
                 scope.Dispose();
-                Dispatcher.UIThread.UpdateServices();
+                Dispatcher.ResetForUnitTests();
             });
         }
 
@@ -71,7 +71,7 @@ namespace Avalonia.UnitTests
                 .Bind<IPlatformRenderInterface>().ToConstant(Services.RenderInterface)
                 .Bind<IFontManagerImpl>().ToConstant(Services.FontManagerImpl)
                 .Bind<ITextShaperImpl>().ToConstant(Services.TextShaperImpl)
-                .Bind<IPlatformThreadingInterface>().ToConstant(Services.ThreadingInterface)
+                .Bind<IDispatcherImpl>().ToConstant(Services.DispatcherImpl)
                 .Bind<ICursorFactory>().ToConstant(Services.StandardCursorFactory)
                 .Bind<IWindowingPlatform>().ToConstant(Services.WindowingPlatform)
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();