diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index d4cde99240..76620e8b93 100644 --- a/Avalonia.Desktop.slnf +++ b/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", diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 5440b76bfe..fdc144e3a5 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -598,13 +598,13 @@ { [[_markedText mutableString] setString:@""]; + [[self inputContext] discardMarkedText]; + if(!_parent->InputMethod->IsActive()){ return; } _parent->InputMethod->Client->SetPreeditText(nullptr); - - [[self inputContext] discardMarkedText]; } - (NSArray *)validAttributesForMarkedText @@ -619,17 +619,12 @@ - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - //[_text replaceCharactersInRange:replacementRange withString:string]; - [self unmarkText]; - - //if(!_lastKeyHandled) - //{ - if(_parent != nullptr) - { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); - } - //} + + if(_parent != nullptr) + { + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + } [[self inputContext] invalidateCharacterCoordinates]; } diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 6d5bd4aa02..d80df68fea 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -1,193 +1,266 @@ #include "common.h" class PlatformThreadingInterface; + + +class LoopCancellation : public ComSingleObject +{ +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 _callback; + ComPtr _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 { private: + ComPtr _events; Signaler* _signaler; - bool _wasRunningAtLeastOnce = false; - - class LoopCancellation : public ComSingleObject - { - 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 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(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* _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 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() { diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 649256ba83..b759720cf2 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -28,6 +28,41 @@ + + + 24 + 18 + 12 + 9 + + + + + + + + + + + + + + + + + + + + + throw new NotSupportedException(); - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { if (interval.TotalMilliseconds < 10) diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index ea84dc84bd..ed88b73149 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -125,7 +125,7 @@ namespace Avalonia.Layout AvaloniaProperty.Register(nameof(VerticalAlignment)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty UseLayoutRoundingProperty = AvaloniaProperty.Register(nameof(UseLayoutRounding), defaultValue: true, inherits: true); diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 74e70b2a14..f06f272e51 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -309,7 +309,7 @@ namespace Avalonia.Media if (input.Length == 3 || input.Length == 4) { var extendedLength = 2 * input.Length; - + #if !BUILDTASK Span extended = stackalloc char[extendedLength]; #else diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index cc9bc6f30a..2e8d8e415d 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -119,9 +119,7 @@ namespace Avalonia.Media } } - var familyName = fontFamily.FamilyNames.PrimaryFamilyName.ToUpperInvariant(); - - if (fontCollection != null && fontCollection.TryGetGlyphTypeface(familyName, + if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { return true; @@ -135,7 +133,7 @@ namespace Avalonia.Media foreach (var familyName in fontFamily.FamilyNames) { - if (SystemFonts.TryGetGlyphTypeface(familyName.ToUpperInvariant(), typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { return true; } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 3350358d68..f2fb490592 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -42,13 +43,11 @@ namespace Avalonia.Media.Fonts if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) { - var familyName = glyphTypeface.FamilyName.ToUpperInvariant(); - - if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) { glyphTypefaces = new ConcurrentDictionary(); - if (_glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) + if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) { _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); } @@ -87,8 +86,6 @@ namespace Avalonia.Media.Fonts public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - familyName = familyName.ToUpperInvariant(); - var key = new FontCollectionKey(style, weight, stretch); if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) @@ -104,11 +101,9 @@ namespace Avalonia.Media.Fonts { var fontFamily = _fontFamilies[i]; - if (fontFamily.Name.ToUpperInvariant().StartsWith(familyName.ToUpperInvariant())) + if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture))) { - familyName = fontFamily.Name.ToUpperInvariant(); - - if (_glyphTypefaceCache.TryGetValue(familyName, out glyphTypefaces) && + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) { return true; diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 1687deb37b..fd332c6ebe 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -42,8 +42,6 @@ namespace Avalonia.Media.Fonts familyName = _fontManager.DefaultFontFamilyName; } - familyName = familyName.ToUpperInvariant(); - var key = new FontCollectionKey(style, weight, stretch); if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index f97457c54d..df68252065 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -131,7 +131,7 @@ namespace Avalonia.Media /// /// /// - /// 0 is a shade of gray (no color). + /// 0 is fully white (or a shade of gray) and shows no color. /// 1 is the full color. /// /// diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs index fc7c174ed6..c90c4cb5ac 100644 --- a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs @@ -228,6 +228,44 @@ static unsafe class PixelFormatReader public void Reset(IntPtr address) => _address = (Rgba64*)address; } + + public unsafe struct Rgb24PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + _address += 3; + return new Rgba8888Pixel + { + R = addr[0], + G = addr[1], + B = addr[2], + A = 255, + }; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgr24PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + _address += 3; + return new Rgba8888Pixel + { + R = addr[2], + G = addr[1], + B = addr[0], + A = 255, + }; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst, PixelFormat format) @@ -242,6 +280,10 @@ static unsafe class PixelFormatReader Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Gray16) Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Rgb24) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Bgr24) + Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Gray32Float) Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Rgba64) @@ -258,7 +300,9 @@ static unsafe class PixelFormatReader || format == PixelFormats.Gray8 || format == PixelFormats.Gray16 || format == PixelFormats.Gray32Float - || format == PixelFormats.Rgba64; + || format == PixelFormats.Rgba64 + || format == PixelFormats.Bgr24 + || format == PixelFormats.Rgb24; } public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader diff --git a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs index 3dbc7c1bb2..4fe4a2e6b9 100644 --- a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs @@ -11,8 +11,6 @@ namespace Avalonia.Platform [Unstable] public interface IPlatformThreadingInterface { - void RunLoop(CancellationToken cancellationToken); - /// /// Starts a timer. /// diff --git a/src/Avalonia.Base/Platform/PixelFormat.cs b/src/Avalonia.Base/Platform/PixelFormat.cs index 99fe17055d..95f49bdb25 100644 --- a/src/Avalonia.Base/Platform/PixelFormat.cs +++ b/src/Avalonia.Base/Platform/PixelFormat.cs @@ -13,7 +13,9 @@ namespace Avalonia.Platform Gray8, Gray16, Gray32Float, - Rgba64 + Rgba64, + Rgb24, + Bgr24 } public record struct PixelFormat @@ -35,6 +37,8 @@ namespace Avalonia.Platform else if (FormatEnum == PixelFormatEnum.Rgb565 || FormatEnum == PixelFormatEnum.Gray16) return 16; + else if (FormatEnum is PixelFormatEnum.Bgr24 or PixelFormatEnum.Rgb24) + return 24; else if (FormatEnum == PixelFormatEnum.Rgba64) return 64; @@ -70,5 +74,7 @@ namespace Avalonia.Platform public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8); public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16); public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); + public static PixelFormat Rgb24 { get; } = new PixelFormat(PixelFormatEnum.Rgb24); + public static PixelFormat Bgr24 { get; } = new PixelFormat(PixelFormatEnum.Bgr24); } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 11a4dd7893..7e9f9ae9ba 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -54,9 +54,9 @@ namespace Avalonia.PropertyStore /// public void BeginReevaluation(bool clearLocalValue = false) { - if (clearLocalValue || Priority != BindingPriority.LocalValue) + if (clearLocalValue || (Priority != BindingPriority.LocalValue && !IsOverridenCurrentValue)) Priority = BindingPriority.Unset; - if (clearLocalValue || BasePriority != BindingPriority.LocalValue) + if (clearLocalValue || (BasePriority != BindingPriority.LocalValue && !IsOverridenCurrentValue)) BasePriority = BindingPriority.Unset; } diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 53cd3ff307..af31459a98 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -395,7 +395,7 @@ namespace Avalonia.PropertyStore if (TryGetEffectiveValue(property, out var existing)) { if (priority <= existing.BasePriority) - ReevaluateEffectiveValue(property, existing); + ReevaluateEffectiveValue(property, existing, changedValueEntry: entry); } else { @@ -774,6 +774,7 @@ namespace Avalonia.PropertyStore private void ReevaluateEffectiveValue( AvaloniaProperty property, EffectiveValue? current, + IValueEntry? changedValueEntry = null, bool ignoreLocalValue = false) { ++_isEvaluating; @@ -796,6 +797,12 @@ namespace Avalonia.PropertyStore { var frame = _frames[i]; var priority = frame.Priority; + + // Exit early if the current EffectiveValue has higher priority than this frame. + if (current?.Priority < priority && current?.BasePriority < priority) + break; + + // Try to get an entry from the frame for the property we're reevaluating. var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged); // If the active state of the frame has changed since the last read, and @@ -803,20 +810,17 @@ namespace Avalonia.PropertyStore // effective values of all properties. if (activeChanged && frame.EntryCount > 1) { - ReevaluateEffectiveValues(); + ReevaluateEffectiveValues(changedValueEntry); return; } - // We're interested in the value if: - // - There is no current effective value, or - // - The value's priority is higher than the current effective value's priority, or - // - The value is a non-animation value and its priority is higher than the current - // effective value's base priority - var isRelevantPriority = current is null || - (priority < current.Priority && priority < current.BasePriority) || - (priority > BindingPriority.Animation && priority < current.BasePriority); - - if (foundEntry && isRelevantPriority && entry!.HasValue) + // If the frame has an entry for this property with a higher priority than the + // current effective value (and that entry has a value), then we have a new + // value for the property. Note that the check for entry.HasValue must be + // evaluated last as it can cause bindings to be subscribed. + if (foundEntry && + HasHigherPriority(entry!, priority, current, changedValueEntry) && + entry!.HasValue) { if (current is not null) { @@ -832,10 +836,6 @@ namespace Avalonia.PropertyStore if (generation != _frameGeneration) goto restart; - - if (current?.Priority < BindingPriority.Unset && - current?.BasePriority < BindingPriority.Unset) - break; } if (current?.Priority == BindingPriority.Unset) @@ -859,7 +859,7 @@ namespace Avalonia.PropertyStore } } - private void ReevaluateEffectiveValues() + private void ReevaluateEffectiveValues(IValueEntry? changedValueEntry = null) { ++_isEvaluating; @@ -894,10 +894,9 @@ namespace Avalonia.PropertyStore { var entry = frame.GetEntry(j); var property = entry.Property; - - // Skip if we already have a value/base value for this property. - if (_effectiveValues.TryGetValue(property, out var effectiveValue) && - effectiveValue.BasePriority < BindingPriority.Unset) + _effectiveValues.TryGetValue(property, out var effectiveValue); + + if (!HasHigherPriority(entry, priority, effectiveValue, changedValueEntry)) continue; if (!entry.HasValue) @@ -942,6 +941,37 @@ namespace Avalonia.PropertyStore } } + private static bool HasHigherPriority( + IValueEntry entry, + BindingPriority entryPriority, + EffectiveValue? current, + IValueEntry? changedValueEntry) + { + // Set the value if: there is no current effective value; or + if (current is null) + return true; + + // The value's priority is higher than the current effective value's priority; or + if (entryPriority < current.Priority && entryPriority < current.BasePriority) + return true; + + // - The value's priority is equal to the current effective value's priority + // - But the effective value was set via SetCurrentValue + // - As long as the SetCurrentValue wasn't overriding the value from the value entry under consideration + // - Or if it was, the value entry under consideration has changed; or + if (entryPriority == current.Priority && + current.IsOverridenCurrentValue && + (current.ValueEntry != entry || entry == changedValueEntry)) + return true; + + // The value is a non-animation value and its priority is higher than the current effective value's base + // priority. + if (entryPriority > BindingPriority.Animation && entryPriority < current.BasePriority) + return true; + + return false; + } + private bool TryGetEffectiveValue( AvaloniaProperty property, [NotNullWhen(true)] out EffectiveValue? value) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index f24f449551..032a3046d7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -30,7 +30,8 @@ internal abstract class BatchStreamPoolBase : IDisposable GC.SuppressFinalize(needsFinalize); var updateRef = new WeakReference>(this); - if (AvaloniaLocator.Current.GetService() == null) + if (AvaloniaLocator.Current.GetService() == null + && AvaloniaLocator.Current.GetService() == null) _reclaimImmediately = true; else StartUpdateTimer(startTimer, updateRef); diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 2cade55f32..1b29cf32f7 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -30,7 +30,7 @@ namespace Avalonia.Threading /// public override void Post(SendOrPostCallback d, object? state) { - Dispatcher.UIThread.Post(d, state, DispatcherPriority.Background); + Dispatcher.UIThread.Post(d, state, DispatcherPriority.Background); } /// diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs new file mode 100644 index 0000000000..80b62b3818 --- /dev/null +++ b/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 +{ + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// Note that the default priority is DispatcherPriority.Send. + /// + public void Invoke(Action callback) + { + Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + public void Invoke(Action callback, DispatcherPriority priority) + { + Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// 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. + /// + public void Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken) + { + Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Action synchronously on the thread that + /// the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// 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. + /// + /// + /// The minimum amount of time to wait for the operation to start. + /// Once the operation has started, it will complete before this method + /// returns. + /// + 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); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The return value from the delegate being invoked. + /// + /// + /// Note that the default priority is DispatcherPriority.Send. + /// + public TResult Invoke(Func callback) + { + return Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// The return value from the delegate being invoked. + /// + public TResult Invoke(Func callback, DispatcherPriority priority) + { + return Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// 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. + /// + /// + /// The return value from the delegate being invoked. + /// + public TResult Invoke(Func callback, DispatcherPriority priority, + CancellationToken cancellationToken) + { + return Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1)); + } + + /// + /// Executes the specified Func synchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// 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. + /// + /// + /// The minimum amount of time to wait for the operation to start. + /// Once the operation has started, it will complete before this method + /// returns. + /// + /// + /// The return value from the delegate being invoked. + /// + public TResult Invoke(Func 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 operation = new DispatcherOperation(this, priority, callback); + return (TResult)InvokeImpl(operation, cancellationToken, timeout)!; + } + + /// + /// Executes the specified Action asynchronously on the thread + /// that the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + /// + /// Note that the default priority is DispatcherPriority.Normal. + /// + public DispatcherOperation InvokeAsync(Action callback) + { + return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + } + + /// + /// Executes the specified Action asynchronously on the thread + /// that the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority) + { + return InvokeAsync(callback, priority, CancellationToken.None); + } + + /// + /// Executes the specified Action asynchronously on the thread + /// that the Dispatcher was created on. + /// + /// + /// An Action delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// 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. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + 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; + } + + /// + /// Executes the specified Func asynchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + /// + /// Note that the default priority is DispatcherPriority.Normal. + /// + public DispatcherOperation InvokeAsync(Func callback) + { + return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + } + + /// + /// Executes the specified Func asynchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + public DispatcherOperation InvokeAsync(Func callback, DispatcherPriority priority) + { + return InvokeAsync(callback, priority, CancellationToken.None); + } + + /// + /// Executes the specified Func asynchronously on the + /// thread that the Dispatcher was created on. + /// + /// + /// A Func delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// 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. + /// + /// + /// An operation representing the queued delegate to be invoked. + /// + public DispatcherOperation InvokeAsync(Func callback, DispatcherPriority priority, + CancellationToken cancellationToken) + { + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + DispatcherPriority.Validate(priority, "priority"); + + DispatcherOperation operation = new DispatcherOperation(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; + } + + /// + public void Post(Action action, DispatcherPriority priority = default) + { + _ = action ?? throw new ArgumentNullException(nameof(action)); + InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None); + } + + /// + /// Posts an action that will be invoked on the dispatcher thread. + /// + /// The method. + /// The argument of method to call. + /// The priority with which to invoke the method. + 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); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs new file mode 100644 index 0000000000..c91af1a514 --- /dev/null +++ b/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(); + } + } + + /// + /// Force-runs all dispatcher operations ignoring any pending OS events, use with caution + /// + 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; + } +} diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs new file mode 100644 index 0000000000..bb252b7f55 --- /dev/null +++ b/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 _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? 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 SnapshotTimersForUnitTests() => + s_uiThread!._timers.ToList(); +} diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 571f782813..25a4a4ce2c 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/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; + +/// +/// Provides services for managing work items on a thread. +/// +/// +/// In Avalonia, there is usually only a single in the application - +/// the one for the UI thread, retrieved via the property. +/// +public partial class Dispatcher : IDispatcher { - /// - /// Provides services for managing work items on a thread. - /// - /// - /// In Avalonia, there is usually only a single in the application - - /// the one for the UI thread, retrieved via the property. - /// - 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()); - - public Dispatcher(IPlatformThreadingInterface? platform) - { - _platform = platform; - _jobRunner = new JobRunner(platform); - - if (_platform != null) - { - _platform.Signaled += _jobRunner.RunJobs; - } - } - - /// - /// Checks that the current thread is the UI thread. - /// - public bool CheckAccess() => _platform?.CurrentThreadIsLoopThread ?? true; - - /// - /// Checks that the current thread is the UI thread and throws if not. - /// - /// - /// The current thread is not the UI thread. - /// - public void VerifyAccess() - { - if (!CheckAccess()) - throw new InvalidOperationException("Call from invalid thread"); - } - - /// - /// Runs the dispatcher's main loop. - /// - /// - /// A cancellation token used to exit the main loop. - /// - public void MainLoop(CancellationToken cancellationToken) - { - var platform = AvaloniaLocator.Current.GetRequiredService(); - cancellationToken.Register(() => platform.Signal(DispatcherPriority.Send)); - platform.RunLoop(cancellationToken); - } - - /// - /// Runs continuations pushed on the loop. - /// - public void RunJobs() - { - _jobRunner.RunJobs(null); - } - - /// - /// Use this method to ensure that more prioritized tasks are executed - /// - /// - public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); - - /// - /// Use this method to check if there are more prioritized tasks - /// - /// - public bool HasJobsWithPriority(DispatcherPriority minimumPriority) => - _jobRunner.HasJobsWithPriority(minimumPriority); - - /// - public Task InvokeAsync(Action action, DispatcherPriority priority = default) - { - _ = action ?? throw new ArgumentNullException(nameof(action)); - return _jobRunner.InvokeAsync(action, priority); - } - - /// - public Task InvokeAsync(Func function, DispatcherPriority priority = default) - { - _ = function ?? throw new ArgumentNullException(nameof(function)); - return _jobRunner.InvokeAsync(function, priority); - } - - /// - public Task InvokeAsync(Func 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(); - /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = default) + private static Dispatcher CreateUIThreadDispatcher() + { + var impl = AvaloniaLocator.Current.GetService(); + if (impl == null) { - _ = function ?? throw new ArgumentNullException(nameof(function)); - return _jobRunner.InvokeAsync(function, priority).Unwrap(); + var platformThreading = AvaloniaLocator.Current.GetService(); + if (platformThreading != null) + impl = new LegacyDispatcherImpl(platformThreading); + else + impl = new NullDispatcherImpl(); } + return new Dispatcher(impl); + } - /// - public void Post(Action action, DispatcherPriority priority = default) - { - _ = action ?? throw new ArgumentNullException(nameof(action)); - _jobRunner.Post(action, priority); - } + /// + /// Checks that the current thread is the UI thread. + /// + public bool CheckAccess() => _impl?.CurrentThreadIsLoopThread ?? true; - /// - public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default) + /// + /// Checks that the current thread is the UI thread and throws if not. + /// + /// + /// The current thread is not the UI thread. + /// + 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"); - /// - /// 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 - /// - /// - internal void EnsurePriority(DispatcherPriority currentPriority) - { - if (currentPriority == DispatcherPriority.MaxValue) - return; - currentPriority += 1; - _jobRunner.RunJobs(currentPriority); + ThrowVerifyAccess(); } + } - /// - /// Allows unit tests to change the platform threading interface. - /// - 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(); - _jobRunner.UpdateServices(); - - if (_platform != null) + if(operation != null) { - _platform.Signaled += _jobRunner.RunJobs; + operation.Abort(); } - } + } while(operation != null); + _impl.UpdateTimer(null); + _hasShutdownFinished = true; + } + + /// + /// Runs the dispatcher's main loop. + /// + /// + /// A cancellation token used to exit the main loop. + /// + public void MainLoop(CancellationToken cancellationToken) + { + if (_controlledImpl == null) + throw new PlatformNotSupportedException(); + cancellationToken.Register(() => RequestProcessing()); + _controlledImpl.RunLoop(cancellationToken); } } diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs new file mode 100644 index 0000000000..173ab81ef8 --- /dev/null +++ b/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; + } + + /// + /// An event that is raised when the operation is aborted or canceled. + /// + public event EventHandler Aborted + { + add + { + lock (Dispatcher.InstanceLock) + { + _aborted += value; + } + } + + remove + { + lock(Dispatcher.InstanceLock) + { + _aborted -= value; + } + } + } + + /// + /// An event that is raised when the operation completes. + /// + /// + /// 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. + /// + 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(); + + /// + /// Returns an awaiter for awaiting the completion of the operation. + /// + /// + /// This method is intended to be used by compilers. + /// + [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 tcs) + tcs.SetResult(null); + } + } + catch (Exception e) + { + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + if (TaskSource is TaskCompletionSource tcs) + tcs.SetException(e); + } + + if (ThrowOnUiThread) + throw; + } + } + + internal virtual object? GetResult() => null; + + protected virtual void AbortTask() => (TaskSource as TaskCompletionSource)?.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 tcs) + TaskSource = tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + return tcs.Task; + } + } +} + +public class DispatcherOperation : DispatcherOperation +{ + public DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Func callback) : base(dispatcher, priority, false) + { + TaskSource = new TaskCompletionSource(); + Callback = callback; + } + + private TaskCompletionSource TaskCompletionSource => (TaskCompletionSource)TaskSource!; + + public new Task 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)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 tcs) + tcs.SetResult(null); + } + } + catch (Exception e) + { + lock (Dispatcher.InstanceLock) + { + Status = DispatcherOperationStatus.Completed; + if (TaskSource is TaskCompletionSource tcs) + tcs.SetException(e); + } + + if (ThrowOnUiThread) + throw; + } + } +} + +public enum DispatcherOperationStatus +{ + Pending = 0, + Aborted = 1, + Completed = 2, + Executing = 3, +} diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index b3194e249b..a71140d288 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -18,44 +18,62 @@ namespace Avalonia.Threading } /// - /// Minimum possible priority + /// The lowest foreground dispatcher priority /// - public static readonly DispatcherPriority MinValue = new(0); - + internal static readonly DispatcherPriority Default = new(0); + /// - /// The job will be processed when the system is idle. + /// The job will be processed with the same priority as input. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue; - + public static readonly DispatcherPriority Input = new(Default - 1); + /// - /// The job will be processed when the application is idle. + /// The job will be processed after other non-idle operations have completed. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue; - + public static readonly DispatcherPriority Background = new(Input - 1); + /// /// The job will be processed after background operations have completed. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue; - + public static readonly DispatcherPriority ContextIdle = new(Background - 1); + + /// - /// The job will be processed with normal priority. + /// The job will be processed when the application is idle. /// - public static readonly DispatcherPriority Normal = MinValue; - + public static readonly DispatcherPriority ApplicationIdle = new (ContextIdle - 1); + /// - /// The job will be processed after other non-idle operations have completed. + /// The job will be processed when the system is idle. /// - public static readonly DispatcherPriority Background = new(MinValue + 1); + public static readonly DispatcherPriority SystemIdle = new(ApplicationIdle - 1); /// - /// The job will be processed with the same priority as input. + /// Minimum possible priority that's actually dispatched, default value /// - public static readonly DispatcherPriority Input = new(Background + 1); + internal static readonly DispatcherPriority MinimumActiveValue = new(SystemIdle); + + /// + /// A dispatcher priority for jobs that shouldn't be executed yet + /// + public static readonly DispatcherPriority Inactive = new(MinimumActiveValue - 1); + + /// + /// Minimum valid priority + /// + internal static readonly DispatcherPriority MinValue = new(Inactive); + + /// + /// Used internally in dispatcher code + /// + public static readonly DispatcherPriority Invalid = new(MinimumActiveValue - 2); + + /// /// The job will be processed after layout and render but before input. /// - public static readonly DispatcherPriority Loaded = new(Input + 1); + public static readonly DispatcherPriority Loaded = new(Default + 1); /// /// The job will be processed with the same priority as render. @@ -80,12 +98,19 @@ namespace Avalonia.Threading /// /// The job will be processed with the same priority as data binding. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue; + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = new(Layout); + + /// + /// The job will be processed with normal priority. + /// +#pragma warning disable CS0618 + public static readonly DispatcherPriority Normal = new(DataBind + 1); +#pragma warning restore CS0618 /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(Layout + 1); + public static readonly DispatcherPriority Send = new(Normal + 1); /// /// Maximum possible priority @@ -127,5 +152,48 @@ namespace Avalonia.Threading /// 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); + } + +#pragma warning disable CS0618 + public override string ToString() + { + if (this == Invalid) + return nameof(Invalid); + if (this == Inactive) + return nameof(Inactive); + if (this == SystemIdle) + return nameof(SystemIdle); + if (this == ContextIdle) + return nameof(ContextIdle); + if (this == ApplicationIdle) + return nameof(ApplicationIdle); + if (this == Background) + return nameof(Background); + if (this == Input) + return nameof(Input); + if (this == Default) + return nameof(Default); + if (this == Loaded) + return nameof(Loaded); + if (this == Render) + return nameof(Render); + if (this == Composition) + return nameof(Composition); + if (this == PreComposition) + return nameof(PreComposition); + if (this == DataBind) + return nameof(DataBind); + if (this == Normal) + return nameof(Normal); + if (this == Send) + return nameof(Send); + return Value.ToString(); + } +#pragma warning restore CS0618 } } diff --git a/src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs b/src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs new file mode 100644 index 0000000000..524b4fab8d --- /dev/null +++ b/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 _priorityChains; // NOTE: should be Priority + private readonly Stack _cacheReusableChains; + + // Sequential chain... + private DispatcherOperation? _head; + private DispatcherOperation? _tail; + + public DispatcherPriorityQueue() + { + // Build the collection of priority chains. + _priorityChains = new SortedList(); // NOTE: should be Priority + _cacheReusableChains = new Stack(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; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index f81229eb48..879d9d8a5f 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -1,207 +1,352 @@ using System; using Avalonia.Reactive; -using Avalonia.Platform; -namespace Avalonia.Threading +namespace Avalonia.Threading; + +/// +/// A timer that is integrated into the Dispatcher queues, and will +/// be processed after a given amount of time at a specified priority. +/// +public partial class DispatcherTimer { /// - /// A timer that uses a to fire at a specified interval. + /// Creates a timer that uses theUI thread's Dispatcher2 to + /// process the timer event at background priority. /// - public class DispatcherTimer + public DispatcherTimer() : this(DispatcherPriority.Background) { - private IDisposable? _timer; + } - private readonly DispatcherPriority _priority; + /// + /// Creates a timer that uses the UI thread's Dispatcher2 to + /// process the timer event at the specified priority. + /// + /// + /// The priority to process the timer at. + /// + public DispatcherTimer(DispatcherPriority priority) : this(Threading.Dispatcher.UIThread, priority, + TimeSpan.FromMilliseconds(0)) + { + } - private TimeSpan _interval; + /// + /// Creates a timer that uses the specified Dispatcher2 to + /// process the timer event at the specified priority. + /// + /// + /// The priority to process the timer at. + /// + /// + /// The dispatcher to use to process the timer. + /// + internal DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher) : this(dispatcher, priority, + TimeSpan.FromMilliseconds(0)) + { + } - /// - /// Initializes a new instance of the class. - /// - public DispatcherTimer() : this(DispatcherPriority.Background) + /// + /// Creates a timer that uses the UI thread's Dispatcher2 to + /// process the timer event at the specified priority after the specified timeout. + /// + /// + /// The interval to tick the timer after. + /// + /// + /// The priority to process the timer at. + /// + /// + /// The callback to call when the timer ticks. + /// + public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) + : this(Threading.Dispatcher.UIThread, priority, interval) + { + if (callback == null) { + throw new ArgumentNullException("callback"); } - /// - /// Initializes a new instance of the class. - /// - /// The priority to use. - public DispatcherTimer(DispatcherPriority priority) - { - _priority = priority; - } + Tick += callback; + Start(); + } - /// - /// Initializes a new instance of the class. - /// - /// The interval at which to tick. - /// The priority to use. - /// The event to call when the timer ticks. - public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) : this(priority) - { - _priority = priority; - Interval = interval; - Tick += callback; - } + /// + /// Gets the dispatcher this timer is associated with. + /// + public Dispatcher Dispatcher + { + get { return _dispatcher; } + } - /// - /// Finalizes an instance of the class. - /// - ~DispatcherTimer() + /// + /// Gets or sets whether the timer is running. + /// + public bool IsEnabled + { + get { return _isEnabled; } + + set { - if (_timer != null) + lock (_instanceLock) { - Stop(); + if (!value && _isEnabled) + { + Stop(); + } + else if (value && !_isEnabled) + { + Start(); + } } } + } - /// - /// Raised when the timer ticks. - /// - public event EventHandler? Tick; + /// + /// Gets or sets the time between timer ticks. + /// + public TimeSpan Interval + { + get { return _interval; } - /// - /// Gets or sets the interval at which the timer ticks. - /// - 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(); } } + } - /// - /// Gets or sets a value indicating whether the timer is running. - /// - public bool IsEnabled + /// + /// Starts the timer. + /// + public void Start() + { + lock (_instanceLock) { - get + if (!_isEnabled) { - return _timer != null; + _isEnabled = true; + + Restart(); } + } + } - set + /// + /// Stops the timer. + /// + 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; } } } - /// - /// Gets or sets user-defined data associated with the timer. - /// - public object? Tag + if (updateOSTimer) { - get; - set; + _dispatcher.RemoveTimer(this); } + } + + /// + /// Starts a new timer. + /// + /// + /// The method to call on timer tick. If the method returns false, the timer will stop. + /// + /// The interval at which to tick. + /// The priority to use. + /// An used to cancel the timer. + public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default) + { + var timer = new DispatcherTimer(priority) { Interval = interval }; - /// - /// Starts a new timer. - /// - /// - /// The method to call on timer tick. If the method returns false, the timer will stop. - /// - /// The interval at which to tick. - /// The priority to use. - /// An used to cancel the timer. - public static IDisposable Run(Func 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()); + } + + /// + /// Runs a method once, after the specified interval. + /// + /// + /// The method to call after the interval has elapsed. + /// + /// The interval after which to call the method. + /// The priority to use. + /// An used to cancel the timer. + 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 }; - /// - /// Runs a method once, after the specified interval. - /// - /// - /// The method to call after the interval has elapsed. - /// - /// The interval after which to call the method. - /// The priority to use. - /// An used to cancel the timer. - 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()); + } + + /// + /// Occurs when the specified timer interval has elapsed and the + /// timer is enabled. + /// + public event EventHandler? Tick; + + /// + /// Any data that the caller wants to pass along with the timer. + /// + 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"); } - /// - /// Starts the timer. - /// - 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(); - _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); } } + } - /// - /// Stops the timer. - /// - 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); + } - /// - /// Raises the event on the dispatcher thread. - /// - 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; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index 1c700132b7..e2a3115f9c 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -26,30 +26,5 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. void Post(Action action, DispatcherPriority priority = default); - - /// - /// Posts an action that will be invoked on the dispatcher thread. - /// - /// The method. - /// The argument of method to call. - /// The priority with which to invoke the method. - void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default); - - /// - /// Invokes a action on the dispatcher thread. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - Task InvokeAsync(Action action, DispatcherPriority priority = default); - - /// - /// Queues the specified work to run on the dispatcher thread and returns a proxy for the - /// task returned by . - /// - /// The work to execute asynchronously. - /// The priority with which to invoke the method. - /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func function, DispatcherPriority priority = default); } } diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs new file mode 100644 index 0000000000..4c30e2eb2c --- /dev/null +++ b/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) + { + + } +} diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs deleted file mode 100644 index ced3423c27..0000000000 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ /dev/null @@ -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 -{ - /// - /// A main loop in a . - /// - internal class JobRunner - { - private IPlatformThreadingInterface? _platform; - - private readonly Queue[] _queues = Enumerable.Range(0, (int)DispatcherPriority.MaxValue + 1) - .Select(_ => new Queue()).ToArray(); - - public JobRunner(IPlatformThreadingInterface? platform) - { - _platform = platform; - } - - /// - /// Runs continuations pushed on the loop. - /// - /// Priority to execute jobs for. Pass null if platform doesn't have internal priority system - public void RunJobs(DispatcherPriority? priority) - { - var minimumPriority = priority ?? DispatcherPriority.MinValue; - while (true) - { - var job = GetNextJob(minimumPriority); - if (job == null) - return; - - job.Run(); - } - } - - /// - /// Invokes a method on the main loop. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - public Task InvokeAsync(Action action, DispatcherPriority priority) - { - var job = new Job(action, priority, false); - AddJob(job); - return job.Task!; - } - - /// - /// Invokes a method on the main loop. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - public Task InvokeAsync(Func function, DispatcherPriority priority) - { - var job = new JobWithResult(function, priority); - AddJob(job); - return job.Task; - } - - /// - /// Post action that will be invoked on main thread - /// - /// The method. - /// - /// The priority with which to invoke the method. - internal void Post(Action action, DispatcherPriority priority) - { - AddJob(new Job(action, priority, true)); - } - - /// - /// Post action that will be invoked on main thread - /// - /// The method to call. - /// The parameter of method to call. - /// The priority with which to invoke the method. - internal void Post(SendOrPostCallback action, object? parameter, DispatcherPriority priority) - { - AddJob(new JobWithArg(action, parameter, priority, true)); - } - - /// - /// Allows unit tests to change the platform threading interface. - /// - internal void UpdateServices() - { - _platform = AvaloniaLocator.Current.GetService(); - } - - 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 - { - /// - /// Gets the job priority. - /// - DispatcherPriority Priority { get; } - - /// - /// Runs the job. - /// - void Run(); - } - - /// - /// A job to run. - /// - private sealed class Job : IJob - { - /// - /// The method to call. - /// - private readonly Action _action; - /// - /// The task completion source. - /// - private readonly TaskCompletionSource? _taskCompletionSource; - - /// - /// Initializes a new instance of the class. - /// - /// The method to call. - /// The job priority. - /// Do not wrap exception in TaskCompletionSource - public Job(Action action, DispatcherPriority priority, bool throwOnUiThread) - { - _action = action; - Priority = priority; - _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); - } - - /// - public DispatcherPriority Priority { get; } - - /// - /// The task. - /// - public Task? Task => _taskCompletionSource?.Task; - - /// - void IJob.Run() - { - if (_taskCompletionSource == null) - { - _action(); - return; - } - try - { - _action(); - _taskCompletionSource.SetResult(null); - } - catch (Exception e) - { - _taskCompletionSource.SetException(e); - } - } - } - - /// - /// A typed job to run. - /// - private sealed class JobWithArg : IJob - { - private readonly SendOrPostCallback _action; - private readonly object? _parameter; - private readonly TaskCompletionSource? _taskCompletionSource; - - /// - /// Initializes a new instance of the class. - /// - /// The method to call. - /// The parameter of method to call. - /// The job priority. - /// Do not wrap exception in TaskCompletionSource - - public JobWithArg(SendOrPostCallback action, object? parameter, DispatcherPriority priority, bool throwOnUiThread) - { - _action = action; - _parameter = parameter; - Priority = priority; - _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); - } - - /// - public DispatcherPriority Priority { get; } - - /// - void IJob.Run() - { - if (_taskCompletionSource == null) - { - _action(_parameter); - return; - } - try - { - _action(_parameter); - _taskCompletionSource.SetResult(default); - } - catch (Exception e) - { - _taskCompletionSource.SetException(e); - } - } - } - - /// - /// A job to run thath return value. - /// - /// Type of job result - private sealed class JobWithResult : IJob - { - private readonly Func _function; - private readonly TaskCompletionSource _taskCompletionSource; - - /// - /// Initializes a new instance of the class. - /// - /// The method to call. - /// The job priority. - public JobWithResult(Func function, DispatcherPriority priority) - { - _function = function; - Priority = priority; - _taskCompletionSource = new TaskCompletionSource(); - } - - /// - public DispatcherPriority Priority { get; } - - /// - /// The task. - /// - public Task Task => _taskCompletionSource.Task; - - /// - void IJob.Run() - { - try - { - var result = _function(); - _taskCompletionSource.SetResult(result); - } - catch (Exception e) - { - _taskCompletionSource.SetException(e); - } - } - } - } -} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index df0c5b100f..b4c5b2a1d2 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -50,7 +50,7 @@ namespace Avalonia AvaloniaProperty.Register(nameof(Clip)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsVisibleProperty = AvaloniaProperty.Register(nameof(IsVisible), true); diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 01cb745ba7..92d8535272 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -1,6 +1,4 @@ -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls +namespace Avalonia.Controls { /// /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 6f49430505..58702ecb61 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives if (accentStep != 0) { // ColorChanged will be invoked in OnPropertyChanged if the value is different - HsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); + SetCurrentValue(HsvColorProperty, AccentColorConverter.GetAccent(hsvColor, accentStep)); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index dd5e7d5b01..ce47a797ec 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -96,8 +96,22 @@ namespace Avalonia.Controls.Primitives // independent pixels of controls. var scale = LayoutHelper.GetLayoutScale(this); - var pixelWidth = Convert.ToInt32(Bounds.Width * scale); - var pixelHeight = Convert.ToInt32(Bounds.Height * scale); + int pixelWidth; + int pixelHeight; + + if (base._track != null) + { + pixelWidth = Convert.ToInt32(base._track.Bounds.Width * scale); + pixelHeight = Convert.ToInt32(base._track.Bounds.Height * scale); + } + else + { + // As a fallback, attempt to calculate using the overall control size + // This shouldn't happen as a track is a required template part of a slider + // However, if it does, the spectrum will still be shown + pixelWidth = Convert.ToInt32(Bounds.Width * scale); + pixelHeight = Convert.ToInt32(Bounds.Height * scale); + } if (pixelWidth != 0 && pixelHeight != 0) { @@ -373,7 +387,7 @@ namespace Avalonia.Controls.Primitives ignorePropertyChanged = true; // Always keep the two color properties in sync - HsvColor = Color.ToHsv(); + SetCurrentValue(HsvColorProperty, Color.ToHsv()); SetColorToSliderValues(); UpdateBackground(); @@ -403,7 +417,7 @@ namespace Avalonia.Controls.Primitives ignorePropertyChanged = true; // Always keep the two color properties in sync - Color = HsvColor.ToRgb(); + SetCurrentValue(ColorProperty, HsvColor.ToRgb()); SetColorToSliderValues(); UpdateBackground(); @@ -440,13 +454,13 @@ namespace Avalonia.Controls.Primitives if (ColorModel == ColorModel.Hsva) { - HsvColor = hsvColor; - Color = hsvColor.ToRgb(); + SetCurrentValue(HsvColorProperty, hsvColor); + SetCurrentValue(ColorProperty, hsvColor.ToRgb()); } else { - Color = color; - HsvColor = color.ToHsv(); + SetCurrentValue(ColorProperty, color); + SetCurrentValue(HsvColorProperty, color.ToHsv()); } UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 5c7de2459b..2245eb8022 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -96,10 +96,10 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty ThirdComponentProperty = - AvaloniaProperty.Register( + public static readonly DirectProperty ThirdComponentProperty = + AvaloniaProperty.RegisterDirect( nameof(ThirdComponent), - ColorComponent.Component3); // Value + o => o.ThirdComponent); /// /// Gets or sets the currently selected color in the RGB color model. @@ -239,8 +239,8 @@ namespace Avalonia.Controls.Primitives /// public ColorComponent ThirdComponent { - get => GetValue(ThirdComponentProperty); - protected set => SetValue(ThirdComponentProperty, value); + get => _thirdComponent; + private set => SetAndRaise(ThirdComponentProperty, ref _thirdComponent, value); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 6f4c0003a8..9198a2f237 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -13,9 +13,9 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.Reactive; namespace Avalonia.Controls.Primitives { @@ -48,6 +48,7 @@ namespace Avalonia.Controls.Primitives private bool _isPointerPressed = false; private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); + private ColorComponent _thirdComponent = ColorComponent.Component3; // HsvComponent.Value private IDisposable? _layoutRootDisposable; private IDisposable? _selectionEllipsePanelDisposable; @@ -403,7 +404,7 @@ namespace Avalonia.Controls.Primitives _updatingHsvColor = true; Hsv newHsv = (new Rgb(color)).ToHsv(); - HsvColor = newHsv.ToHsvColor(color.A / 255.0); + SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(color.A / 255.0)); _updatingHsvColor = false; UpdateEllipse(); @@ -534,7 +535,7 @@ namespace Avalonia.Controls.Primitives _updatingColor = true; Rgb newRgb = (new Hsv(hsvColor)).ToRgb(); - Color = newRgb.ToColor(hsvColor.A); + SetCurrentValue(ColorProperty, newRgb.ToColor(hsvColor.A)); _updatingColor = false; @@ -608,8 +609,8 @@ namespace Avalonia.Controls.Primitives Rgb newRgb = newHsv.ToRgb(); double alpha = HsvColor.A; - Color = newRgb.ToColor(alpha); - HsvColor = newHsv.ToHsvColor(alpha); + SetCurrentValue(ColorProperty, newRgb.ToColor(alpha)); + SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha)); UpdateEllipse(); UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs b/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs new file mode 100644 index 0000000000..4f3ae46a24 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Controls +{ + /// + /// Defines the position of a color's alpha component relative to all other components. + /// + public enum AlphaComponentPosition + { + /// + /// The alpha component occurs before all other components. + /// + /// + /// For example, this may indicate the #AARRGGBB or ARGB format which + /// is the default format for XAML itself and the Color struct. + /// + Leading, + + /// + /// The alpha component occurs after all other components. + /// + /// + /// For example, this may indicate the #RRGGBBAA or RGBA format which + /// is the default format for CSS. + /// + Trailing, + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index b76059037b..e334a1d323 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -42,6 +42,14 @@ namespace Avalonia.Controls nameof(ColorSpectrumShape), ColorSpectrumShape.Box); + /// + /// Defines the property. + /// + public static readonly StyledProperty HexInputAlphaPositionProperty = + AvaloniaProperty.Register( + nameof(HexInputAlphaPosition), + AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI + /// /// Defines the property. /// @@ -260,6 +268,16 @@ namespace Avalonia.Controls set => SetValue(ColorSpectrumShapeProperty, value); } + /// + /// Gets or sets the position of the alpha component in the hexadecimal input box relative to + /// all other color components. + /// + public AlphaComponentPosition HexInputAlphaPosition + { + get => GetValue(HexInputAlphaPositionProperty); + set => SetValue(HexInputAlphaPositionProperty, value); + } + /// public HsvColor HsvColor { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 977b1f5c84..274e7f5851 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; -using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Media; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -28,8 +24,7 @@ namespace Avalonia.Controls private TextBox? _hexTextBox; private TabControl? _tabControl; - private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); - protected bool ignorePropertyChanged = false; + protected bool _ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -46,11 +41,11 @@ namespace Avalonia.Controls { if (_hexTextBox != null) { - var convertedColor = colorToHexConverter.ConvertBack(_hexTextBox.Text, typeof(Color), null, CultureInfo.CurrentCulture); + var convertedColor = ColorToHexConverter.ParseHexString(_hexTextBox.Text ?? string.Empty, HexInputAlphaPosition); if (convertedColor is Color color) { - Color = color; + SetCurrentValue(ColorProperty, color); } // Re-apply the hex value @@ -66,7 +61,7 @@ namespace Avalonia.Controls { if (_hexTextBox != null) { - _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + _hexTextBox.Text = ColorToHexConverter.ToHexString(Color, HexInputAlphaPosition); } } @@ -167,7 +162,7 @@ namespace Avalonia.Controls // The work-around for this is done here where SelectedIndex is forcefully // synchronized with whatever the TabControl property value is. This is // possible since selection validation is already done by this method. - SelectedIndex = _tabControl.SelectedIndex; + SetCurrentValue(SelectedIndexProperty, _tabControl.SelectedIndex); } return; @@ -200,7 +195,7 @@ namespace Avalonia.Controls /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (ignorePropertyChanged) + if (_ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -209,29 +204,29 @@ namespace Avalonia.Controls // Always keep the two color properties in sync if (change.Property == ColorProperty) { - ignorePropertyChanged = true; + _ignorePropertyChanged = true; - HsvColor = Color.ToHsv(); + SetCurrentValue(HsvColorProperty, Color.ToHsv()); SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), change.GetNewValue())); - ignorePropertyChanged = false; + _ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - ignorePropertyChanged = true; + _ignorePropertyChanged = true; - Color = HsvColor.ToRgb(); + SetCurrentValue(ColorProperty, HsvColor.ToRgb()); SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - ignorePropertyChanged = false; + _ignorePropertyChanged = false; } else if (change.Property == PaletteProperty) { @@ -241,7 +236,7 @@ namespace Avalonia.Controls // bound properties controlling the palette grid if (palette != null) { - PaletteColumnCount = palette.ColorCount; + SetCurrentValue(PaletteColumnCountProperty, palette.ColorCount); List newPaletteColors = new List(); for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) @@ -252,14 +247,14 @@ namespace Avalonia.Controls } } - PaletteColors = newPaletteColors; + SetCurrentValue(PaletteColorsProperty, newPaletteColors); } } else if (change.Property == IsAlphaEnabledProperty) { // Manually coerce the HsvColor value // (Color will be coerced automatically if HsvColor changes) - HsvColor = OnCoerceHsvColor(HsvColor); + SetCurrentValue(HsvColorProperty, OnCoerceHsvColor(HsvColor)); } else if (change.Property == IsColorComponentsVisibleProperty || change.Property == IsColorPaletteVisibleProperty || diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 8d5f2332be..8798f874f4 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -2,6 +2,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls.Converters { @@ -10,6 +11,11 @@ namespace Avalonia.Controls.Converters /// public class ColorToHexConverter : IValueConverter { + /// + /// Gets or sets the position of a color's alpha component relative to all other components. + /// + public AlphaComponentPosition AlphaPosition { get; set; } = AlphaComponentPosition.Leading; + /// public object? Convert( object? value, @@ -42,16 +48,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); - - if (includeSymbol == false) - { - // TODO: When .net standard 2.0 is dropped, replace the below line - //hexColor = hexColor.Replace("#", string.Empty, StringComparison.Ordinal); - hexColor = hexColor.Replace("#", string.Empty); - } - - return hexColor; + return ToHexString(color, AlphaPosition, includeSymbol); } /// @@ -62,21 +59,159 @@ namespace Avalonia.Controls.Converters CultureInfo culture) { string hexValue = value?.ToString() ?? string.Empty; + return ParseHexString(hexValue, AlphaPosition) ?? AvaloniaProperty.UnsetValue; + } + + /// + /// Converts the given color to its hex color value string representation. + /// + /// The color to represent as a hex value string. + /// The output position of the alpha component. + /// Whether the hex symbol '#' will be added. + /// The input color converted to its hex value string. + public static string ToHexString( + Color color, + AlphaComponentPosition alphaPosition, + bool includeSymbol = false) + { + uint intColor; + if (alphaPosition == AlphaComponentPosition.Trailing) + { + intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + } + else + { + // Default is Leading alpha + intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + } - if (Color.TryParse(hexValue, out Color color)) + string hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); + + if (includeSymbol) + { + hexColor = '#' + hexColor; + } + + return hexColor; + } + + /// + /// Parses a hex color value string into a new . + /// + /// The hex color string to parse. + /// The input position of the alpha component. + /// The parsed ; otherwise, null. + public static Color? ParseHexString( + string hexColor, + AlphaComponentPosition alphaPosition) + { + hexColor = hexColor.Trim(); + + if (!hexColor.StartsWith("#", StringComparison.Ordinal)) + { + hexColor = "#" + hexColor; + } + + if (TryParseHexFormat(hexColor.AsSpan(), alphaPosition, out Color color)) { return color; } - else if (hexValue.StartsWith("#", StringComparison.Ordinal) == false && - Color.TryParse("#" + hexValue, out Color color2)) + + return null; + } + + /// + /// Parses the given span of characters representing a hex color value into a new . + /// + /// + /// This is based on the Color.TryParseHexFormat() method. + /// It is copied because it needs to be extended to handle alpha position. + /// However, the alpha position enum is only available in the controls namespace with the ColorPicker control. + /// + private static bool TryParseHexFormat( + ReadOnlySpan s, + AlphaComponentPosition alphaPosition, + out Color color) + { + static bool TryParseCore(ReadOnlySpan input, AlphaComponentPosition alphaPosition, ref Color color) { - return color2; + var alphaComponent = 0u; + + if (input.Length == 6) + { + if (alphaPosition == AlphaComponentPosition.Trailing) + { + alphaComponent = 0x000000FF; + } + else + { + alphaComponent = 0xFF000000; + } + } + else if (input.Length != 8) + { + return false; + } + + if (!input.TryParseUInt(NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed)) + { + return false; + } + + if (alphaComponent != 0) + { + if (alphaPosition == AlphaComponentPosition.Trailing) + { + parsed = (parsed << 8) | alphaComponent; + } + else + { + parsed = parsed | alphaComponent; + } + } + + if (alphaPosition == AlphaComponentPosition.Trailing) + { + // #RRGGBBAA + color = new Color( + a: (byte)(parsed & 0xFF), + r: (byte)((parsed >> 24) & 0xFF), + g: (byte)((parsed >> 16) & 0xFF), + b: (byte)((parsed >> 8) & 0xFF)); + } + else + { + // #AARRGGBB + color = new Color( + a: (byte)((parsed >> 24) & 0xFF), + r: (byte)((parsed >> 16) & 0xFF), + g: (byte)((parsed >> 8) & 0xFF), + b: (byte)(parsed & 0xFF)); + } + + return true; } - else + + color = default; + + ReadOnlySpan input = s.Slice(1); + + // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB). + if (input.Length == 3 || input.Length == 4) { - // Invalid hex color value provided - return AvaloniaProperty.UnsetValue; + var extendedLength = 2 * input.Length; + Span extended = stackalloc char[extendedLength]; + + for (int i = 0; i < input.Length; i++) + { + extended[2 * i + 0] = input[i]; + extended[2 * i + 1] = input[i]; + } + + return TryParseCore(extended, alphaPosition, ref color); } + + return TryParseCore(input, alphaPosition, ref color); } } } diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index c1a03b1b77..c9801c432b 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -1,6 +1,6 @@ using System; -using System.Globalization; using System.Collections.Generic; +using System.Globalization; using Avalonia.Media; using Avalonia.Utilities; @@ -11,8 +11,11 @@ namespace Avalonia.Controls.Primitives /// public static class ColorHelper { - private static readonly Dictionary cachedDisplayNames = new Dictionary(); - private static readonly object cacheMutex = new object(); + private static readonly Dictionary _cachedDisplayNames = new Dictionary(); + private static readonly Dictionary _cachedKnownColorNames = new Dictionary(); + private static readonly object _displayNameCacheMutex = new object(); + private static readonly object _knownColorCacheMutex = new object(); + private static readonly KnownColor[] _knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); /// /// Gets the relative (perceptual) luminance/brightness of the given color. @@ -59,7 +62,36 @@ namespace Avalonia.Controls.Primitives /// The approximate color display name. public static string ToDisplayName(Color color) { - // Without rounding, there are 16,777,216 possible RGB colors (without alpha). + var hsvColor = color.ToHsv(); + + // Handle extremes that are outside the below algorithm + if (color.A == 0x00) + { + return GetDisplayName(KnownColor.Transparent); + } + + // HSV ---------------------------------------------------------------------- + // + // There are far too many possible HSV colors to cache and search through + // for performance reasons. Therefore, the HSV color is rounded. + // Rounding is tolerable in this algorithm because it is perception based. + // Hue is the most important for user perception so is rounded the least. + // Then there is a lot of loss in rounding the saturation and value components + // which are not as closely related to perceived color. + // + // Hue : Round to nearest int (0..360) + // Saturation : Round to the nearest 1/10 (0..1) + // Value : Round to the nearest 1/10 (0..1) + // Alpha : Is ignored in this algorithm + // + // Rounding results in ~36_000 values to cache in the worse case. + // + // RGB ---------------------------------------------------------------------- + // + // The original algorithm worked in RGB color space. + // If this code is every adjusted to work in RGB again note the following: + // + // Without rounding, there are 16_777_216 possible RGB colors (without alpha). // This is too many to cache and search through for performance reasons. // It is also needlessly large as there are only ~140 known/named colors. // Therefore, rounding of the input color's component values is done to @@ -68,42 +100,67 @@ namespace Avalonia.Controls.Primitives // The rounding value of 5 is specially chosen. // It is a factor of 255 and therefore evenly divisible which improves // the quality of the calculations. - double rounding = 5; - var roundedColor = new Color( - 0xFF, - Convert.ToByte(Math.Round(color.R / rounding) * rounding), - Convert.ToByte(Math.Round(color.G / rounding) * rounding), - Convert.ToByte(Math.Round(color.B / rounding) * rounding)); + var roundedHsvColor = new HsvColor( + 1.0, + Math.Round(hsvColor.H, 0), + Math.Round(hsvColor.S, 1), + Math.Round(hsvColor.V, 1)); // Attempt to use a previously cached display name - lock (cacheMutex) + lock (_displayNameCacheMutex) { - if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) + if (_cachedDisplayNames.TryGetValue(roundedHsvColor, out var displayName)) { return displayName; } } + // Build the KnownColor name cache if it doesn't already exist + lock (_knownColorCacheMutex) + { + if (_cachedKnownColorNames.Count == 0) + { + for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1 + { + KnownColor knownColor = _knownColors[i]; + + // Some known colors have the same numerical value. For example: + // - Aqua = 0xff00ffff + // - Cyan = 0xff00ffff + // + // This is not possible to represent in a dictionary which requires + // unique values. Therefore, only the first value is used. + + if (!_cachedKnownColorNames.ContainsKey(knownColor)) + { + _cachedKnownColorNames.Add(knownColor, GetDisplayName(knownColor)); + } + } + } + } + // Find the closest known color by measuring 3D Euclidean distance (ignore alpha) + // This is done in HSV color space to most closely match user-perception var closestKnownColor = KnownColor.None; var closestKnownColorDistance = double.PositiveInfinity; - var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); - for (int i = 1; i < knownColors.Length; i++) // Skip 'None' + for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1 { + KnownColor knownColor = _knownColors[i]; + // Transparent is skipped since alpha is ignored making it equivalent to White - if (knownColors[i] != KnownColor.Transparent) + if (knownColor != KnownColor.Transparent) { - Color knownColor = KnownColors.ToColor(knownColors[i]); + HsvColor knownHsvColor = KnownColors.ToColor(knownColor).ToHsv(); double distance = Math.Sqrt( - Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) + - Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) + - Math.Pow((double)(roundedColor.B - knownColor.B), 2.0)); + Math.Pow((roundedHsvColor.H - knownHsvColor.H), 2.0) + + Math.Pow((roundedHsvColor.S - knownHsvColor.S), 2.0) + + Math.Pow((roundedHsvColor.V - knownHsvColor.V), 2.0)); if (distance < closestKnownColorDistance) { - closestKnownColor = knownColors[i]; + closestKnownColor = knownColor; closestKnownColorDistance = distance; } } @@ -113,26 +170,19 @@ namespace Avalonia.Controls.Primitives // Cache results for next time as well if (closestKnownColor != KnownColor.None) { - var sb = StringBuilderCache.Acquire(); - string name = closestKnownColor.ToString(); + string? displayName; - // Add spaces converting PascalCase to human-readable names - for (int i = 0; i < name.Length; i++) + lock (_knownColorCacheMutex) { - if (i != 0 && - char.IsUpper(name[i])) + if (!_cachedKnownColorNames.TryGetValue(closestKnownColor, out displayName)) { - sb.Append(' '); + displayName = GetDisplayName(closestKnownColor); } - - sb.Append(name[i]); } - string displayName = StringBuilderCache.GetStringAndRelease(sb); - - lock (cacheMutex) + lock (_displayNameCacheMutex) { - cachedDisplayNames.Add(roundedColor, displayName); + _cachedDisplayNames.Add(roundedHsvColor, displayName); } return displayName; @@ -142,5 +192,35 @@ namespace Avalonia.Controls.Primitives return string.Empty; } } + + /// + /// Gets the human-readable display name for the given . + /// + /// + /// This currently uses the enum value's C# name directly + /// which limits it to the EN language only. In the future this should be localized + /// to other cultures. + /// + /// The to get the display name for. + /// The human-readable display name for the given . + private static string GetDisplayName(KnownColor knownColor) + { + var sb = StringBuilderCache.Acquire(); + string name = knownColor.ToString(); + + // Add spaces converting PascalCase to human-readable names + for (int i = 0; i < name.Length; i++) + { + if (i != 0 && + char.IsUpper(name[i])) + { + sb.Append(' '); + } + + sb.Append(name[i]); + } + + return StringBuilderCache.GetStringAndRelease(sb); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index 819d745772..dbd92d4ac5 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -4,8 +4,6 @@ // Licensed to The Avalonia Project under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Layout; diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 807e4de0b1..a9f52b93c7 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -42,7 +42,8 @@ - + + 20 + 20 + 10 + 10 + - + @@ -25,27 +32,28 @@