Browse Source

Merge branch 'master' into markup-nullable

pull/10608/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
b2d27b2d8b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      Avalonia.Desktop.slnf
  2. 19
      native/Avalonia.Native/src/OSX/AvnView.mm
  3. 341
      native/Avalonia.Native/src/OSX/platformthreading.mm
  4. 35
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  5. 2
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  6. 2
      src/Avalonia.Base/Layout/Layoutable.cs
  7. 2
      src/Avalonia.Base/Media/Color.cs
  8. 6
      src/Avalonia.Base/Media/FontManager.cs
  9. 15
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  10. 2
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  11. 2
      src/Avalonia.Base/Media/HsvColor.cs
  12. 46
      src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs
  13. 2
      src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs
  14. 8
      src/Avalonia.Base/Platform/PixelFormat.cs
  15. 4
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  16. 72
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  17. 3
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  18. 2
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  19. 553
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  20. 238
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  21. 207
      src/Avalonia.Base/Threading/Dispatcher.Timers.cs
  22. 234
      src/Avalonia.Base/Threading/Dispatcher.cs
  23. 308
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  24. 110
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  25. 418
      src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs
  26. 423
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  27. 25
      src/Avalonia.Base/Threading/IDispatcher.cs
  28. 103
      src/Avalonia.Base/Threading/IDispatcherImpl.cs
  29. 300
      src/Avalonia.Base/Threading/JobRunner.cs
  30. 2
      src/Avalonia.Base/Visual.cs
  31. 4
      src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
  32. 2
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  33. 30
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  34. 10
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  35. 11
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  36. 26
      src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs
  37. 18
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
  38. 35
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs
  39. 169
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  40. 146
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  41. 2
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  42. 5
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  43. 67
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
  44. 3
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  45. 4
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  46. 67
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml
  47. 3
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  48. 2
      src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs
  49. 104
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  50. 65
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  51. 92
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  52. 109
      src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
  53. 2
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  54. 2
      src/Avalonia.Controls/ScrollViewer.cs
  55. 10
      src/Avalonia.Controls/Slider.cs
  56. 230
      src/Avalonia.Controls/SplitView/SplitView.cs
  57. 2
      src/Avalonia.Controls/TopLevel.cs
  58. 4
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  59. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Bold.otf
  60. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Bold.ttf
  61. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.otf
  62. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.ttf
  63. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Light.otf
  64. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Light.ttf
  65. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Medium.otf
  66. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Medium.ttf
  67. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Regular.otf
  68. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf
  69. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.otf
  70. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.ttf
  71. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Thin.otf
  72. BIN
      src/Avalonia.Fonts.Inter/Assets/Inter-Thin.ttf
  73. 2
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  74. 86
      src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs
  75. 5
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  76. 13
      src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs
  77. 7
      src/Avalonia.Native/CallbackBase.cs
  78. 132
      src/Avalonia.Native/DispatcherImpl.cs
  79. 115
      src/Avalonia.Native/PlatformThreadingInterface.cs
  80. 16
      src/Avalonia.Native/avn.idl
  81. 4
      src/Avalonia.X11/X11Platform.cs
  82. 159
      src/Avalonia.X11/X11PlatformThreading.cs
  83. 3
      src/Avalonia.X11/X11Window.cs
  84. 5
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  85. 9
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  86. 11
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
  87. 4
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  88. 12
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  89. 109
      src/Shared/RawEventGrouping.cs
  90. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  91. 2
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  92. 43
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  93. 121
      src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs
  94. 99
      src/Windows/Avalonia.Win32/Win32Platform.cs
  95. 5
      src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs
  96. 20
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  97. 6
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  98. 148
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs
  99. 48
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs
  100. 13
      tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs

7
Avalonia.Desktop.slnf

@ -38,13 +38,14 @@
"src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj", "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\tools\\Avalonia.Generators\\Avalonia.Generators.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\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.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.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",

19
native/Avalonia.Native/src/OSX/AvnView.mm

@ -598,13 +598,13 @@
{ {
[[_markedText mutableString] setString:@""]; [[_markedText mutableString] setString:@""];
[[self inputContext] discardMarkedText];
if(!_parent->InputMethod->IsActive()){ if(!_parent->InputMethod->IsActive()){
return; return;
} }
_parent->InputMethod->Client->SetPreeditText(nullptr); _parent->InputMethod->Client->SetPreeditText(nullptr);
[[self inputContext] discardMarkedText];
} }
- (NSArray<NSString *> *)validAttributesForMarkedText - (NSArray<NSString *> *)validAttributesForMarkedText
@ -619,17 +619,12 @@
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange - (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{ {
//[_text replaceCharactersInRange:replacementRange withString:string];
[self unmarkText]; [self unmarkText];
//if(!_lastKeyHandled) if(_parent != nullptr)
//{ {
if(_parent != nullptr) _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
{ }
_lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
}
//}
[[self inputContext] invalidateCharacterCoordinates]; [[self inputContext] invalidateCharacterCoordinates];
} }

341
native/Avalonia.Native/src/OSX/platformthreading.mm

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

35
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -28,6 +28,41 @@
<Grid Grid.Column="2" <Grid Grid.Column="2"
Grid.Row="0" Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid.Resources>
<x:Double x:Key="ColorSliderSize">24</x:Double>
<x:Double x:Key="ColorSliderTrackSize">18</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">12</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">9</CornerRadius>
<!-- Due to 'SystemControlForegroundBaseHighBrush' usage this only works in Fluent theme. -->
<!-- Otherwise it would be necessary to make custom light/dark resources. -->
<ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderThickness" Value="5" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Ellipse Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Fill="Transparent"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</ControlTheme>
</Grid.Resources>
<ColorSpectrum x:Name="ColorSpectrum1" <ColorSpectrum x:Name="ColorSpectrum1"
Grid.Row="0" Grid.Row="0"
Color="Red" Color="Red"

2
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

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

2
src/Avalonia.Base/Layout/Layoutable.cs

@ -125,7 +125,7 @@ namespace Avalonia.Layout
AvaloniaProperty.Register<Layoutable, VerticalAlignment>(nameof(VerticalAlignment)); AvaloniaProperty.Register<Layoutable, VerticalAlignment>(nameof(VerticalAlignment));
/// <summary> /// <summary>
/// Defines the <see cref="UseLayoutRoundingProperty"/> property. /// Defines the <see cref="UseLayoutRounding"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> UseLayoutRoundingProperty = public static readonly StyledProperty<bool> UseLayoutRoundingProperty =
AvaloniaProperty.Register<Layoutable, bool>(nameof(UseLayoutRounding), defaultValue: true, inherits: true); AvaloniaProperty.Register<Layoutable, bool>(nameof(UseLayoutRounding), defaultValue: true, inherits: true);

2
src/Avalonia.Base/Media/Color.cs

@ -309,7 +309,7 @@ namespace Avalonia.Media
if (input.Length == 3 || input.Length == 4) if (input.Length == 3 || input.Length == 4)
{ {
var extendedLength = 2 * input.Length; var extendedLength = 2 * input.Length;
#if !BUILDTASK #if !BUILDTASK
Span<char> extended = stackalloc char[extendedLength]; Span<char> extended = stackalloc char[extendedLength];
#else #else

6
src/Avalonia.Base/Media/FontManager.cs

@ -119,9 +119,7 @@ namespace Avalonia.Media
} }
} }
var familyName = fontFamily.FamilyNames.PrimaryFamilyName.ToUpperInvariant(); if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(familyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{ {
return true; return true;
@ -135,7 +133,7 @@ namespace Avalonia.Media
foreach (var familyName in fontFamily.FamilyNames) 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; return true;
} }

15
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
@ -42,13 +43,11 @@ namespace Avalonia.Media.Fonts
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
{ {
var familyName = glyphTypeface.FamilyName.ToUpperInvariant(); if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{ {
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>(); glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
if (_glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
{ {
_fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
} }
@ -87,8 +86,6 @@ namespace Avalonia.Media.Fonts
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{ {
familyName = familyName.ToUpperInvariant();
var key = new FontCollectionKey(style, weight, stretch); var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
@ -104,11 +101,9 @@ namespace Avalonia.Media.Fonts
{ {
var fontFamily = _fontFamilies[i]; 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(fontFamily.Name, out glyphTypefaces) &&
if (_glyphTypefaceCache.TryGetValue(familyName, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{ {
return true; return true;

2
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -42,8 +42,6 @@ namespace Avalonia.Media.Fonts
familyName = _fontManager.DefaultFontFamilyName; familyName = _fontManager.DefaultFontFamilyName;
} }
familyName = familyName.ToUpperInvariant();
var key = new FontCollectionKey(style, weight, stretch); var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))

2
src/Avalonia.Base/Media/HsvColor.cs

@ -131,7 +131,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <list type="bullet"> /// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item> /// <item>0 is fully white (or a shade of gray) and shows no color.</item>
/// <item>1 is the full color.</item> /// <item>1 is the full color.</item>
/// </list> /// </list>
/// </remarks> /// </remarks>

46
src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs

@ -228,6 +228,44 @@ static unsafe class PixelFormatReader
public void Reset(IntPtr address) => _address = (Rgba64*)address; 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, public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst,
PixelFormat format) PixelFormat format)
@ -242,6 +280,10 @@ static unsafe class PixelFormatReader
Transcode<Gray8PixelReader>(dst, src, size, strideSrc, strideDst); Transcode<Gray8PixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray16) else if (format == PixelFormats.Gray16)
Transcode<Gray16PixelReader>(dst, src, size, strideSrc, strideDst); Transcode<Gray16PixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Rgb24)
Transcode<Rgb24PixelFormatReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Bgr24)
Transcode<Bgr24PixelFormatReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray32Float) else if (format == PixelFormats.Gray32Float)
Transcode<Gray32FloatPixelReader>(dst, src, size, strideSrc, strideDst); Transcode<Gray32FloatPixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Rgba64) else if (format == PixelFormats.Rgba64)
@ -258,7 +300,9 @@ static unsafe class PixelFormatReader
|| format == PixelFormats.Gray8 || format == PixelFormats.Gray8
|| format == PixelFormats.Gray16 || format == PixelFormats.Gray16
|| format == PixelFormats.Gray32Float || format == PixelFormats.Gray32Float
|| format == PixelFormats.Rgba64; || format == PixelFormats.Rgba64
|| format == PixelFormats.Bgr24
|| format == PixelFormats.Rgb24;
} }
public static void Transcode<TReader>(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader public static void Transcode<TReader>(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader

2
src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs

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

8
src/Avalonia.Base/Platform/PixelFormat.cs

@ -13,7 +13,9 @@ namespace Avalonia.Platform
Gray8, Gray8,
Gray16, Gray16,
Gray32Float, Gray32Float,
Rgba64 Rgba64,
Rgb24,
Bgr24
} }
public record struct PixelFormat public record struct PixelFormat
@ -35,6 +37,8 @@ namespace Avalonia.Platform
else if (FormatEnum == PixelFormatEnum.Rgb565 else if (FormatEnum == PixelFormatEnum.Rgb565
|| FormatEnum == PixelFormatEnum.Gray16) || FormatEnum == PixelFormatEnum.Gray16)
return 16; return 16;
else if (FormatEnum is PixelFormatEnum.Bgr24 or PixelFormatEnum.Rgb24)
return 24;
else if (FormatEnum == PixelFormatEnum.Rgba64) else if (FormatEnum == PixelFormatEnum.Rgba64)
return 64; return 64;
@ -70,5 +74,7 @@ namespace Avalonia.Platform
public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8); public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8);
public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16); public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16);
public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); 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);
} }
} }

4
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -54,9 +54,9 @@ namespace Avalonia.PropertyStore
/// </remarks> /// </remarks>
public void BeginReevaluation(bool clearLocalValue = false) public void BeginReevaluation(bool clearLocalValue = false)
{ {
if (clearLocalValue || Priority != BindingPriority.LocalValue) if (clearLocalValue || (Priority != BindingPriority.LocalValue && !IsOverridenCurrentValue))
Priority = BindingPriority.Unset; Priority = BindingPriority.Unset;
if (clearLocalValue || BasePriority != BindingPriority.LocalValue) if (clearLocalValue || (BasePriority != BindingPriority.LocalValue && !IsOverridenCurrentValue))
BasePriority = BindingPriority.Unset; BasePriority = BindingPriority.Unset;
} }

72
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -395,7 +395,7 @@ namespace Avalonia.PropertyStore
if (TryGetEffectiveValue(property, out var existing)) if (TryGetEffectiveValue(property, out var existing))
{ {
if (priority <= existing.BasePriority) if (priority <= existing.BasePriority)
ReevaluateEffectiveValue(property, existing); ReevaluateEffectiveValue(property, existing, changedValueEntry: entry);
} }
else else
{ {
@ -774,6 +774,7 @@ namespace Avalonia.PropertyStore
private void ReevaluateEffectiveValue( private void ReevaluateEffectiveValue(
AvaloniaProperty property, AvaloniaProperty property,
EffectiveValue? current, EffectiveValue? current,
IValueEntry? changedValueEntry = null,
bool ignoreLocalValue = false) bool ignoreLocalValue = false)
{ {
++_isEvaluating; ++_isEvaluating;
@ -796,6 +797,12 @@ namespace Avalonia.PropertyStore
{ {
var frame = _frames[i]; var frame = _frames[i];
var priority = frame.Priority; 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); 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 // 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. // effective values of all properties.
if (activeChanged && frame.EntryCount > 1) if (activeChanged && frame.EntryCount > 1)
{ {
ReevaluateEffectiveValues(); ReevaluateEffectiveValues(changedValueEntry);
return; return;
} }
// We're interested in the value if: // If the frame has an entry for this property with a higher priority than the
// - There is no current effective value, or // current effective value (and that entry has a value), then we have a new
// - The value's priority is higher than the current effective value's priority, or // value for the property. Note that the check for entry.HasValue must be
// - The value is a non-animation value and its priority is higher than the current // evaluated last as it can cause bindings to be subscribed.
// effective value's base priority if (foundEntry &&
var isRelevantPriority = current is null || HasHigherPriority(entry!, priority, current, changedValueEntry) &&
(priority < current.Priority && priority < current.BasePriority) || entry!.HasValue)
(priority > BindingPriority.Animation && priority < current.BasePriority);
if (foundEntry && isRelevantPriority && entry!.HasValue)
{ {
if (current is not null) if (current is not null)
{ {
@ -832,10 +836,6 @@ namespace Avalonia.PropertyStore
if (generation != _frameGeneration) if (generation != _frameGeneration)
goto restart; goto restart;
if (current?.Priority < BindingPriority.Unset &&
current?.BasePriority < BindingPriority.Unset)
break;
} }
if (current?.Priority == BindingPriority.Unset) if (current?.Priority == BindingPriority.Unset)
@ -859,7 +859,7 @@ namespace Avalonia.PropertyStore
} }
} }
private void ReevaluateEffectiveValues() private void ReevaluateEffectiveValues(IValueEntry? changedValueEntry = null)
{ {
++_isEvaluating; ++_isEvaluating;
@ -894,10 +894,9 @@ namespace Avalonia.PropertyStore
{ {
var entry = frame.GetEntry(j); var entry = frame.GetEntry(j);
var property = entry.Property; var property = entry.Property;
_effectiveValues.TryGetValue(property, out var effectiveValue);
// Skip if we already have a value/base value for this property.
if (_effectiveValues.TryGetValue(property, out var effectiveValue) && if (!HasHigherPriority(entry, priority, effectiveValue, changedValueEntry))
effectiveValue.BasePriority < BindingPriority.Unset)
continue; continue;
if (!entry.HasValue) 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( private bool TryGetEffectiveValue(
AvaloniaProperty property, AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value) [NotNullWhen(true)] out EffectiveValue? value)

3
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

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

2
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

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

553
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

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

238
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

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

207
src/Avalonia.Base/Threading/Dispatcher.Timers.cs

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

234
src/Avalonia.Base/Threading/Dispatcher.cs

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

308
src/Avalonia.Base/Threading/DispatcherOperation.cs

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

110
src/Avalonia.Base/Threading/DispatcherPriority.cs

@ -18,44 +18,62 @@ namespace Avalonia.Threading
} }
/// <summary> /// <summary>
/// Minimum possible priority /// The lowest foreground dispatcher priority
/// </summary> /// </summary>
public static readonly DispatcherPriority MinValue = new(0); internal static readonly DispatcherPriority Default = new(0);
/// <summary> /// <summary>
/// The job will be processed when the system is idle. /// The job will be processed with the same priority as input.
/// </summary> /// </summary>
[Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue; public static readonly DispatcherPriority Input = new(Default - 1);
/// <summary> /// <summary>
/// The job will be processed when the application is idle. /// The job will be processed after other non-idle operations have completed.
/// </summary> /// </summary>
[Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue; public static readonly DispatcherPriority Background = new(Input - 1);
/// <summary> /// <summary>
/// The job will be processed after background operations have completed. /// The job will be processed after background operations have completed.
/// </summary> /// </summary>
[Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue; public static readonly DispatcherPriority ContextIdle = new(Background - 1);
/// <summary> /// <summary>
/// The job will be processed with normal priority. /// The job will be processed when the application is idle.
/// </summary> /// </summary>
public static readonly DispatcherPriority Normal = MinValue; public static readonly DispatcherPriority ApplicationIdle = new (ContextIdle - 1);
/// <summary> /// <summary>
/// The job will be processed after other non-idle operations have completed. /// The job will be processed when the system is idle.
/// </summary> /// </summary>
public static readonly DispatcherPriority Background = new(MinValue + 1); public static readonly DispatcherPriority SystemIdle = new(ApplicationIdle - 1);
/// <summary> /// <summary>
/// The job will be processed with the same priority as input. /// Minimum possible priority that's actually dispatched, default value
/// </summary> /// </summary>
public static readonly DispatcherPriority Input = new(Background + 1); internal static readonly DispatcherPriority MinimumActiveValue = new(SystemIdle);
/// <summary>
/// A dispatcher priority for jobs that shouldn't be executed yet
/// </summary>
public static readonly DispatcherPriority Inactive = new(MinimumActiveValue - 1);
/// <summary>
/// Minimum valid priority
/// </summary>
internal static readonly DispatcherPriority MinValue = new(Inactive);
/// <summary>
/// Used internally in dispatcher code
/// </summary>
public static readonly DispatcherPriority Invalid = new(MinimumActiveValue - 2);
/// <summary> /// <summary>
/// The job will be processed after layout and render but before input. /// The job will be processed after layout and render but before input.
/// </summary> /// </summary>
public static readonly DispatcherPriority Loaded = new(Input + 1); public static readonly DispatcherPriority Loaded = new(Default + 1);
/// <summary> /// <summary>
/// The job will be processed with the same priority as render. /// The job will be processed with the same priority as render.
@ -80,12 +98,19 @@ namespace Avalonia.Threading
/// <summary> /// <summary>
/// The job will be processed with the same priority as data binding. /// The job will be processed with the same priority as data binding.
/// </summary> /// </summary>
[Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue; [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = new(Layout);
/// <summary>
/// The job will be processed with normal priority.
/// </summary>
#pragma warning disable CS0618
public static readonly DispatcherPriority Normal = new(DataBind + 1);
#pragma warning restore CS0618
/// <summary> /// <summary>
/// The job will be processed before other asynchronous operations. /// The job will be processed before other asynchronous operations.
/// </summary> /// </summary>
public static readonly DispatcherPriority Send = new(Layout + 1); public static readonly DispatcherPriority Send = new(Normal + 1);
/// <summary> /// <summary>
/// Maximum possible priority /// Maximum possible priority
@ -127,5 +152,48 @@ namespace Avalonia.Threading
/// <inheritdoc /> /// <inheritdoc />
public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value); 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
} }
} }

418
src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs

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

423
src/Avalonia.Base/Threading/DispatcherTimer.cs

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

25
src/Avalonia.Base/Threading/IDispatcher.cs

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

103
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)
{
}
}

300
src/Avalonia.Base/Threading/JobRunner.cs

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

2
src/Avalonia.Base/Visual.cs

@ -50,7 +50,7 @@ namespace Avalonia
AvaloniaProperty.Register<Visual, Geometry?>(nameof(Clip)); AvaloniaProperty.Register<Visual, Geometry?>(nameof(Clip));
/// <summary> /// <summary>
/// Defines the <see cref="IsVisibleProperty"/> property. /// Defines the <see cref="IsVisible"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsVisibleProperty = public static readonly StyledProperty<bool> IsVisibleProperty =
AvaloniaProperty.Register<Visual, bool>(nameof(IsVisible), true); AvaloniaProperty.Register<Visual, bool>(nameof(IsVisible), true);

4
src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs

@ -1,6 +1,4 @@
using Avalonia.Controls.Primitives; namespace Avalonia.Controls
namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down.

2
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs

@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives
if (accentStep != 0) if (accentStep != 0)
{ {
// ColorChanged will be invoked in OnPropertyChanged if the value is different // ColorChanged will be invoked in OnPropertyChanged if the value is different
HsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); SetCurrentValue(HsvColorProperty, AccentColorConverter.GetAccent(hsvColor, accentStep));
} }
} }
} }

30
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs

@ -96,8 +96,22 @@ namespace Avalonia.Controls.Primitives
// independent pixels of controls. // independent pixels of controls.
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var pixelWidth = Convert.ToInt32(Bounds.Width * scale); int pixelWidth;
var pixelHeight = Convert.ToInt32(Bounds.Height * scale); 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) if (pixelWidth != 0 && pixelHeight != 0)
{ {
@ -373,7 +387,7 @@ namespace Avalonia.Controls.Primitives
ignorePropertyChanged = true; ignorePropertyChanged = true;
// Always keep the two color properties in sync // Always keep the two color properties in sync
HsvColor = Color.ToHsv(); SetCurrentValue(HsvColorProperty, Color.ToHsv());
SetColorToSliderValues(); SetColorToSliderValues();
UpdateBackground(); UpdateBackground();
@ -403,7 +417,7 @@ namespace Avalonia.Controls.Primitives
ignorePropertyChanged = true; ignorePropertyChanged = true;
// Always keep the two color properties in sync // Always keep the two color properties in sync
Color = HsvColor.ToRgb(); SetCurrentValue(ColorProperty, HsvColor.ToRgb());
SetColorToSliderValues(); SetColorToSliderValues();
UpdateBackground(); UpdateBackground();
@ -440,13 +454,13 @@ namespace Avalonia.Controls.Primitives
if (ColorModel == ColorModel.Hsva) if (ColorModel == ColorModel.Hsva)
{ {
HsvColor = hsvColor; SetCurrentValue(HsvColorProperty, hsvColor);
Color = hsvColor.ToRgb(); SetCurrentValue(ColorProperty, hsvColor.ToRgb());
} }
else else
{ {
Color = color; SetCurrentValue(ColorProperty, color);
HsvColor = color.ToHsv(); SetCurrentValue(HsvColorProperty, color.ToHsv());
} }
UpdatePseudoClasses(); UpdatePseudoClasses();

10
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@ -96,10 +96,10 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="ThirdComponent"/> property. /// Defines the <see cref="ThirdComponent"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ColorComponent> ThirdComponentProperty = public static readonly DirectProperty<ColorSpectrum, ColorComponent> ThirdComponentProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorComponent>( AvaloniaProperty.RegisterDirect<ColorSpectrum, ColorComponent>(
nameof(ThirdComponent), nameof(ThirdComponent),
ColorComponent.Component3); // Value o => o.ThirdComponent);
/// <summary> /// <summary>
/// Gets or sets the currently selected color in the RGB color model. /// Gets or sets the currently selected color in the RGB color model.
@ -239,8 +239,8 @@ namespace Avalonia.Controls.Primitives
/// </remarks> /// </remarks>
public ColorComponent ThirdComponent public ColorComponent ThirdComponent
{ {
get => GetValue(ThirdComponentProperty); get => _thirdComponent;
protected set => SetValue(ThirdComponentProperty, value); private set => SetAndRaise(ThirdComponentProperty, ref _thirdComponent, value);
} }
} }
} }

11
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@ -13,9 +13,9 @@ using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Reactive;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -48,6 +48,7 @@ namespace Avalonia.Controls.Primitives
private bool _isPointerPressed = false; private bool _isPointerPressed = false;
private bool _shouldShowLargeSelection = false; private bool _shouldShowLargeSelection = false;
private List<Hsv> _hsvValues = new List<Hsv>(); private List<Hsv> _hsvValues = new List<Hsv>();
private ColorComponent _thirdComponent = ColorComponent.Component3; // HsvComponent.Value
private IDisposable? _layoutRootDisposable; private IDisposable? _layoutRootDisposable;
private IDisposable? _selectionEllipsePanelDisposable; private IDisposable? _selectionEllipsePanelDisposable;
@ -403,7 +404,7 @@ namespace Avalonia.Controls.Primitives
_updatingHsvColor = true; _updatingHsvColor = true;
Hsv newHsv = (new Rgb(color)).ToHsv(); Hsv newHsv = (new Rgb(color)).ToHsv();
HsvColor = newHsv.ToHsvColor(color.A / 255.0); SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(color.A / 255.0));
_updatingHsvColor = false; _updatingHsvColor = false;
UpdateEllipse(); UpdateEllipse();
@ -534,7 +535,7 @@ namespace Avalonia.Controls.Primitives
_updatingColor = true; _updatingColor = true;
Rgb newRgb = (new Hsv(hsvColor)).ToRgb(); Rgb newRgb = (new Hsv(hsvColor)).ToRgb();
Color = newRgb.ToColor(hsvColor.A); SetCurrentValue(ColorProperty, newRgb.ToColor(hsvColor.A));
_updatingColor = false; _updatingColor = false;
@ -608,8 +609,8 @@ namespace Avalonia.Controls.Primitives
Rgb newRgb = newHsv.ToRgb(); Rgb newRgb = newHsv.ToRgb();
double alpha = HsvColor.A; double alpha = HsvColor.A;
Color = newRgb.ToColor(alpha); SetCurrentValue(ColorProperty, newRgb.ToColor(alpha));
HsvColor = newHsv.ToHsvColor(alpha); SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha));
UpdateEllipse(); UpdateEllipse();
UpdatePseudoClasses(); UpdatePseudoClasses();

26
src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs

@ -0,0 +1,26 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines the position of a color's alpha component relative to all other components.
/// </summary>
public enum AlphaComponentPosition
{
/// <summary>
/// The alpha component occurs before all other components.
/// </summary>
/// <remarks>
/// For example, this may indicate the #AARRGGBB or ARGB format which
/// is the default format for XAML itself and the Color struct.
/// </remarks>
Leading,
/// <summary>
/// The alpha component occurs after all other components.
/// </summary>
/// <remarks>
/// For example, this may indicate the #RRGGBBAA or RGBA format which
/// is the default format for CSS.
/// </remarks>
Trailing,
}
}

18
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs

@ -42,6 +42,14 @@ namespace Avalonia.Controls
nameof(ColorSpectrumShape), nameof(ColorSpectrumShape),
ColorSpectrumShape.Box); ColorSpectrumShape.Box);
/// <summary>
/// Defines the <see cref="HexInputAlphaPosition"/> property.
/// </summary>
public static readonly StyledProperty<AlphaComponentPosition> HexInputAlphaPositionProperty =
AvaloniaProperty.Register<ColorView, AlphaComponentPosition>(
nameof(HexInputAlphaPosition),
AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI
/// <summary> /// <summary>
/// Defines the <see cref="HsvColor"/> property. /// Defines the <see cref="HsvColor"/> property.
/// </summary> /// </summary>
@ -260,6 +268,16 @@ namespace Avalonia.Controls
set => SetValue(ColorSpectrumShapeProperty, value); set => SetValue(ColorSpectrumShapeProperty, value);
} }
/// <summary>
/// Gets or sets the position of the alpha component in the hexadecimal input box relative to
/// all other color components.
/// </summary>
public AlphaComponentPosition HexInputAlphaPosition
{
get => GetValue(HexInputAlphaPositionProperty);
set => SetValue(HexInputAlphaPositionProperty, value);
}
/// <inheritdoc cref="ColorSpectrum.HsvColor"/> /// <inheritdoc cref="ColorSpectrum.HsvColor"/>
public HsvColor HsvColor public HsvColor HsvColor
{ {

35
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs

@ -1,14 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using Avalonia.Controls.Converters; using Avalonia.Controls.Converters;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -28,8 +24,7 @@ namespace Avalonia.Controls
private TextBox? _hexTextBox; private TextBox? _hexTextBox;
private TabControl? _tabControl; private TabControl? _tabControl;
private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); protected bool _ignorePropertyChanged = false;
protected bool ignorePropertyChanged = false;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ColorView"/> class. /// Initializes a new instance of the <see cref="ColorView"/> class.
@ -46,11 +41,11 @@ namespace Avalonia.Controls
{ {
if (_hexTextBox != null) 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) if (convertedColor is Color color)
{ {
Color = color; SetCurrentValue(ColorProperty, color);
} }
// Re-apply the hex value // Re-apply the hex value
@ -66,7 +61,7 @@ namespace Avalonia.Controls
{ {
if (_hexTextBox != null) 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 // The work-around for this is done here where SelectedIndex is forcefully
// synchronized with whatever the TabControl property value is. This is // synchronized with whatever the TabControl property value is. This is
// possible since selection validation is already done by this method. // possible since selection validation is already done by this method.
SelectedIndex = _tabControl.SelectedIndex; SetCurrentValue(SelectedIndexProperty, _tabControl.SelectedIndex);
} }
return; return;
@ -200,7 +195,7 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (ignorePropertyChanged) if (_ignorePropertyChanged)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
return; return;
@ -209,29 +204,29 @@ namespace Avalonia.Controls
// Always keep the two color properties in sync // Always keep the two color properties in sync
if (change.Property == ColorProperty) if (change.Property == ColorProperty)
{ {
ignorePropertyChanged = true; _ignorePropertyChanged = true;
HsvColor = Color.ToHsv(); SetCurrentValue(HsvColorProperty, Color.ToHsv());
SetColorToHexTextBox(); SetColorToHexTextBox();
OnColorChanged(new ColorChangedEventArgs( OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue<Color>(), change.GetOldValue<Color>(),
change.GetNewValue<Color>())); change.GetNewValue<Color>()));
ignorePropertyChanged = false; _ignorePropertyChanged = false;
} }
else if (change.Property == HsvColorProperty) else if (change.Property == HsvColorProperty)
{ {
ignorePropertyChanged = true; _ignorePropertyChanged = true;
Color = HsvColor.ToRgb(); SetCurrentValue(ColorProperty, HsvColor.ToRgb());
SetColorToHexTextBox(); SetColorToHexTextBox();
OnColorChanged(new ColorChangedEventArgs( OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue<HsvColor>().ToRgb(), change.GetOldValue<HsvColor>().ToRgb(),
change.GetNewValue<HsvColor>().ToRgb())); change.GetNewValue<HsvColor>().ToRgb()));
ignorePropertyChanged = false; _ignorePropertyChanged = false;
} }
else if (change.Property == PaletteProperty) else if (change.Property == PaletteProperty)
{ {
@ -241,7 +236,7 @@ namespace Avalonia.Controls
// bound properties controlling the palette grid // bound properties controlling the palette grid
if (palette != null) if (palette != null)
{ {
PaletteColumnCount = palette.ColorCount; SetCurrentValue(PaletteColumnCountProperty, palette.ColorCount);
List<Color> newPaletteColors = new List<Color>(); List<Color> newPaletteColors = new List<Color>();
for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) 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) else if (change.Property == IsAlphaEnabledProperty)
{ {
// Manually coerce the HsvColor value // Manually coerce the HsvColor value
// (Color will be coerced automatically if HsvColor changes) // (Color will be coerced automatically if HsvColor changes)
HsvColor = OnCoerceHsvColor(HsvColor); SetCurrentValue(HsvColorProperty, OnCoerceHsvColor(HsvColor));
} }
else if (change.Property == IsColorComponentsVisibleProperty || else if (change.Property == IsColorComponentsVisibleProperty ||
change.Property == IsColorPaletteVisibleProperty || change.Property == IsColorPaletteVisibleProperty ||

169
src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs

@ -2,6 +2,7 @@
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters namespace Avalonia.Controls.Converters
{ {
@ -10,6 +11,11 @@ namespace Avalonia.Controls.Converters
/// </summary> /// </summary>
public class ColorToHexConverter : IValueConverter public class ColorToHexConverter : IValueConverter
{ {
/// <summary>
/// Gets or sets the position of a color's alpha component relative to all other components.
/// </summary>
public AlphaComponentPosition AlphaPosition { get; set; } = AlphaComponentPosition.Leading;
/// <inheritdoc/> /// <inheritdoc/>
public object? Convert( public object? Convert(
object? value, object? value,
@ -42,16 +48,7 @@ namespace Avalonia.Controls.Converters
return AvaloniaProperty.UnsetValue; return AvaloniaProperty.UnsetValue;
} }
string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); return ToHexString(color, AlphaPosition, includeSymbol);
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;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -62,21 +59,159 @@ namespace Avalonia.Controls.Converters
CultureInfo culture) CultureInfo culture)
{ {
string hexValue = value?.ToString() ?? string.Empty; string hexValue = value?.ToString() ?? string.Empty;
return ParseHexString(hexValue, AlphaPosition) ?? AvaloniaProperty.UnsetValue;
}
/// <summary>
/// Converts the given color to its hex color value string representation.
/// </summary>
/// <param name="color">The color to represent as a hex value string.</param>
/// <param name="alphaPosition">The output position of the alpha component.</param>
/// <param name="includeSymbol">Whether the hex symbol '#' will be added.</param>
/// <returns>The input color converted to its hex value string.</returns>
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;
}
/// <summary>
/// Parses a hex color value string into a new <see cref="Color"/>.
/// </summary>
/// <param name="hexColor">The hex color string to parse.</param>
/// <param name="alphaPosition">The input position of the alpha component.</param>
/// <returns>The parsed <see cref="Color"/>; otherwise, null.</returns>
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; return color;
} }
else if (hexValue.StartsWith("#", StringComparison.Ordinal) == false &&
Color.TryParse("#" + hexValue, out Color color2)) return null;
}
/// <summary>
/// Parses the given span of characters representing a hex color value into a new <see cref="Color"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private static bool TryParseHexFormat(
ReadOnlySpan<char> s,
AlphaComponentPosition alphaPosition,
out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> 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<char> 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 var extendedLength = 2 * input.Length;
return AvaloniaProperty.UnsetValue; Span<char> 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);
} }
} }
} }

146
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@ -1,6 +1,6 @@
using System; using System;
using System.Globalization;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -11,8 +11,11 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public static class ColorHelper public static class ColorHelper
{ {
private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>(); private static readonly Dictionary<HsvColor, string> _cachedDisplayNames = new Dictionary<HsvColor, string>();
private static readonly object cacheMutex = new object(); private static readonly Dictionary<KnownColor, string> _cachedKnownColorNames = new Dictionary<KnownColor, string>();
private static readonly object _displayNameCacheMutex = new object();
private static readonly object _knownColorCacheMutex = new object();
private static readonly KnownColor[] _knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
/// <summary> /// <summary>
/// Gets the relative (perceptual) luminance/brightness of the given color. /// Gets the relative (perceptual) luminance/brightness of the given color.
@ -59,7 +62,36 @@ namespace Avalonia.Controls.Primitives
/// <returns>The approximate color display name.</returns> /// <returns>The approximate color display name.</returns>
public static string ToDisplayName(Color color) 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. // 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. // 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 // 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. // The rounding value of 5 is specially chosen.
// It is a factor of 255 and therefore evenly divisible which improves // It is a factor of 255 and therefore evenly divisible which improves
// the quality of the calculations. // the quality of the calculations.
double rounding = 5; var roundedHsvColor = new HsvColor(
var roundedColor = new Color( 1.0,
0xFF, Math.Round(hsvColor.H, 0),
Convert.ToByte(Math.Round(color.R / rounding) * rounding), Math.Round(hsvColor.S, 1),
Convert.ToByte(Math.Round(color.G / rounding) * rounding), Math.Round(hsvColor.V, 1));
Convert.ToByte(Math.Round(color.B / rounding) * rounding));
// Attempt to use a previously cached display name // 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; 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) // 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 closestKnownColor = KnownColor.None;
var closestKnownColorDistance = double.PositiveInfinity; 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 // 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( double distance = Math.Sqrt(
Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) + Math.Pow((roundedHsvColor.H - knownHsvColor.H), 2.0) +
Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) + Math.Pow((roundedHsvColor.S - knownHsvColor.S), 2.0) +
Math.Pow((double)(roundedColor.B - knownColor.B), 2.0)); Math.Pow((roundedHsvColor.V - knownHsvColor.V), 2.0));
if (distance < closestKnownColorDistance) if (distance < closestKnownColorDistance)
{ {
closestKnownColor = knownColors[i]; closestKnownColor = knownColor;
closestKnownColorDistance = distance; closestKnownColorDistance = distance;
} }
} }
@ -113,26 +170,19 @@ namespace Avalonia.Controls.Primitives
// Cache results for next time as well // Cache results for next time as well
if (closestKnownColor != KnownColor.None) if (closestKnownColor != KnownColor.None)
{ {
var sb = StringBuilderCache.Acquire(); string? displayName;
string name = closestKnownColor.ToString();
// Add spaces converting PascalCase to human-readable names lock (_knownColorCacheMutex)
for (int i = 0; i < name.Length; i++)
{ {
if (i != 0 && if (!_cachedKnownColorNames.TryGetValue(closestKnownColor, out displayName))
char.IsUpper(name[i]))
{ {
sb.Append(' '); displayName = GetDisplayName(closestKnownColor);
} }
sb.Append(name[i]);
} }
string displayName = StringBuilderCache.GetStringAndRelease(sb); lock (_displayNameCacheMutex)
lock (cacheMutex)
{ {
cachedDisplayNames.Add(roundedColor, displayName); _cachedDisplayNames.Add(roundedHsvColor, displayName);
} }
return displayName; return displayName;
@ -142,5 +192,35 @@ namespace Avalonia.Controls.Primitives
return string.Empty; return string.Empty;
} }
} }
/// <summary>
/// Gets the human-readable display name for the given <see cref="KnownColor"/>.
/// </summary>
/// <remarks>
/// This currently uses the <see cref="KnownColor"/> enum value's C# name directly
/// which limits it to the EN language only. In the future this should be localized
/// to other cultures.
/// </remarks>
/// <param name="knownColor">The <see cref="KnownColor"/> to get the display name for.</param>
/// <returns>The human-readable display name for the given <see cref="KnownColor"/>.</returns>
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);
}
} }
} }

2
src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs

@ -4,8 +4,6 @@
// Licensed to The Avalonia Project under the MIT License. // Licensed to The Avalonia Project under the MIT License.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Layout; using Avalonia.Layout;

5
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@ -42,7 +42,8 @@
</Panel> </Panel>
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="nopadding"> <Flyout FlyoutPresenterClasses="nopadding"
Placement="Top">
<!-- The following is copy-pasted from the ColorView's control template. <!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master). It MUST always be kept in sync with the ColorView (which is master).
@ -216,6 +217,7 @@
Content="RGB" Content="RGB"
CornerRadius="4,0,0,4" CornerRadius="4,0,0,4"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -223,6 +225,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,4,4,0" CornerRadius="0,4,4,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

67
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml

@ -1,13 +1,20 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Note that the Slider thumb should generally follow the overall Slider dimensions.
Therefore, there are not currently separate resources to control it. -->
<x:Double x:Key="ColorSliderSize">20</x:Double>
<x:Double x:Key="ColorSliderTrackSize">20</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">10</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">10</CornerRadius>
<ControlTheme x:Key="ColorSliderThumbTheme" <ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb"> TargetType="Thumb">
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />--> <!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />-->
<Setter Property="BorderThickness" Value="3" /> <Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate> <ControlTemplate>
@ -25,27 +32,28 @@
<Style Selector="^:horizontal"> <Style Selector="^:horizontal">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Height" Value="20" /> <Setter Property="Height" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{TemplateBinding Background}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
Height="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}" Value="{TemplateBinding Value, Mode=TwoWay}"
@ -82,7 +90,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"
@ -97,26 +105,27 @@
<Style Selector="^:vertical"> <Style Selector="^:vertical">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Width" Value="20" /> <Setter Property="Width" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
HorizontalAlignment="Stretch" Width="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
@ -154,7 +163,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"

3
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -8,7 +8,6 @@
<pc:ContrastBrushConverter x:Key="ContrastBrushConverter" /> <pc:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" /> <converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:ColorToHexConverter x:Key="ColorToHexConverter" />
<converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" /> <converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
<globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" /> <globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
<x:Double x:Key="ColorViewTabStripHeight">48</x:Double> <x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
@ -465,6 +464,7 @@
Content="RGB" Content="RGB"
CornerRadius="4,0,0,4" CornerRadius="4,0,0,4"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -472,6 +472,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,4,4,0" CornerRadius="0,4,4,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

4
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -42,7 +42,7 @@
</Panel> </Panel>
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout> <Flyout Placement="Top">
<!-- The following is copy-pasted from the ColorView's control template. <!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master). It MUST always be kept in sync with the ColorView (which is master).
@ -216,6 +216,7 @@
Content="RGB" Content="RGB"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -223,6 +224,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

67
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml

@ -1,13 +1,20 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Note that the Slider thumb should generally follow the overall Slider dimensions.
Therefore, there are not currently separate resources to control it. -->
<x:Double x:Key="ColorSliderSize">20</x:Double>
<x:Double x:Key="ColorSliderTrackSize">20</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">10</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">10</CornerRadius>
<ControlTheme x:Key="ColorSliderThumbTheme" <ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb"> TargetType="Thumb">
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
<!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />--> <!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />-->
<Setter Property="BorderThickness" Value="3" /> <Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate> <ControlTemplate>
@ -25,27 +32,28 @@
<Style Selector="^:horizontal"> <Style Selector="^:horizontal">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Height" Value="20" /> <Setter Property="Height" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{TemplateBinding Background}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
Height="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}" Value="{TemplateBinding Value, Mode=TwoWay}"
@ -82,7 +90,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"
@ -97,26 +105,27 @@
<Style Selector="^:vertical"> <Style Selector="^:vertical">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Width" Value="20" /> <Setter Property="Width" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
HorizontalAlignment="Stretch" Width="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
@ -154,7 +163,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"

3
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -8,7 +8,6 @@
<pc:ContrastBrushConverter x:Key="ContrastBrushConverter" /> <pc:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" /> <converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:ColorToHexConverter x:Key="ColorToHexConverter" />
<converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" /> <converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
<globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" /> <globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
<x:Double x:Key="ColorViewTabStripHeight">48</x:Double> <x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
@ -427,6 +426,7 @@
Content="RGB" Content="RGB"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -434,6 +434,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

2
src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs

@ -113,7 +113,7 @@ namespace Avalonia.Layout
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing)); AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing));
/// <summary> /// <summary>
/// Defines the <see cref="MaximumRowsOrColumnsProperty"/> property. /// Defines the <see cref="MaximumRowsOrColumns"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<int> MaximumRowsOrColumnsProperty = public static readonly StyledProperty<int> MaximumRowsOrColumnsProperty =
AvaloniaProperty.Register<UniformGridLayout, int>(nameof(MaximumRowsOrColumns)); AvaloniaProperty.Register<UniformGridLayout, int>(nameof(MaximumRowsOrColumns));

104
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs

@ -52,34 +52,32 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="IsTextCompletionEnabled" /> property.</value> /// <value>The identifier for the <see cref="IsTextCompletionEnabled" /> property.</value>
public static readonly StyledProperty<bool> IsTextCompletionEnabledProperty = public static readonly StyledProperty<bool> IsTextCompletionEnabledProperty =
AvaloniaProperty.Register<AutoCompleteBox, bool>(nameof(IsTextCompletionEnabled)); AvaloniaProperty.Register<AutoCompleteBox, bool>(
nameof(IsTextCompletionEnabled));
/// <summary> /// <summary>
/// Identifies the <see cref="ItemTemplate" /> property. /// Identifies the <see cref="ItemTemplate" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="ItemTemplate" /> property.</value> /// <value>The identifier for the <see cref="ItemTemplate" /> property.</value>
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty = public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
AvaloniaProperty.Register<AutoCompleteBox, IDataTemplate>(nameof(ItemTemplate)); AvaloniaProperty.Register<AutoCompleteBox, IDataTemplate>(
nameof(ItemTemplate));
/// <summary> /// <summary>
/// Identifies the <see cref="IsDropDownOpen" /> property. /// Identifies the <see cref="IsDropDownOpen" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="IsDropDownOpen" /> property.</value> /// <value>The identifier for the <see cref="IsDropDownOpen" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, bool> IsDropDownOpenProperty = public static readonly StyledProperty<bool> IsDropDownOpenProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, bool>( AvaloniaProperty.Register<AutoCompleteBox, bool>(
nameof(IsDropDownOpen), nameof(IsDropDownOpen));
o => o.IsDropDownOpen,
(o, v) => o.IsDropDownOpen = v);
/// <summary> /// <summary>
/// Identifies the <see cref="SelectedItem" /> property. /// Identifies the <see cref="SelectedItem" /> property.
/// </summary> /// </summary>
/// <value>The identifier the <see cref="SelectedItem" /> property.</value> /// <value>The identifier the <see cref="SelectedItem" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, object?> SelectedItemProperty = public static readonly StyledProperty<object?> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, object?>( AvaloniaProperty.Register<AutoCompleteBox, object?>(
nameof(SelectedItem), nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay, defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true); enableDataValidation: true);
@ -115,58 +113,50 @@ namespace Avalonia.Controls
/// Identifies the <see cref="ItemFilter" /> property. /// Identifies the <see cref="ItemFilter" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="ItemFilter" /> property.</value> /// <value>The identifier for the <see cref="ItemFilter" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, AutoCompleteFilterPredicate<object?>?> ItemFilterProperty = public static readonly StyledProperty<AutoCompleteFilterPredicate<object?>?> ItemFilterProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteFilterPredicate<object?>?>( AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterPredicate<object?>?>(
nameof(ItemFilter), nameof(ItemFilter));
o => o.ItemFilter,
(o, v) => o.ItemFilter = v);
/// <summary> /// <summary>
/// Identifies the <see cref="TextFilter" /> property. /// Identifies the <see cref="TextFilter" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="TextFilter" /> property.</value> /// <value>The identifier for the <see cref="TextFilter" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, AutoCompleteFilterPredicate<string?>?> TextFilterProperty = public static readonly StyledProperty<AutoCompleteFilterPredicate<string?>?> TextFilterProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteFilterPredicate<string?>?>( AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterPredicate<string?>?>(
nameof(TextFilter), nameof(TextFilter),
o => o.TextFilter, defaultValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
(o, v) => o.TextFilter = v,
unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
/// <summary> /// <summary>
/// Identifies the <see cref="ItemSelector" /> property. /// Identifies the <see cref="ItemSelector" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="ItemSelector" /> property.</value> /// <value>The identifier for the <see cref="ItemSelector" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, AutoCompleteSelector<object>?> ItemSelectorProperty = public static readonly StyledProperty<AutoCompleteSelector<object>?> ItemSelectorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteSelector<object>?>( AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteSelector<object>?>(
nameof(ItemSelector), nameof(ItemSelector));
o => o.ItemSelector,
(o, v) => o.ItemSelector = v);
/// <summary> /// <summary>
/// Identifies the <see cref="TextSelector" /> property. /// Identifies the <see cref="TextSelector" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="TextSelector" /> property.</value> /// <value>The identifier for the <see cref="TextSelector" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, AutoCompleteSelector<string?>?> TextSelectorProperty = public static readonly StyledProperty<AutoCompleteSelector<string?>?> TextSelectorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteSelector<string?>?>( AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteSelector<string?>?>(
nameof(TextSelector), nameof(TextSelector));
o => o.TextSelector,
(o, v) => o.TextSelector = v);
/// <summary> /// <summary>
/// Identifies the <see cref="Items" /> property. /// Identifies the <see cref="Items" /> property.
/// </summary> /// </summary>
/// <value>The identifier for the <see cref="Items" /> property.</value> /// <value>The identifier for the <see cref="Items" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, IEnumerable?> ItemsProperty = public static readonly StyledProperty<IEnumerable?> ItemsProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, IEnumerable?>( AvaloniaProperty.Register<AutoCompleteBox, IEnumerable?>(
nameof(Items), nameof(Items));
o => o.Items,
(o, v) => o.Items = v);
public static readonly DirectProperty<AutoCompleteBox, Func<string?, CancellationToken, Task<IEnumerable<object>>>?> AsyncPopulatorProperty = /// <summary>
AvaloniaProperty.RegisterDirect<AutoCompleteBox, Func<string?, CancellationToken, Task<IEnumerable<object>>>?>( /// Identifies the <see cref="AsyncPopulator" /> property.
nameof(AsyncPopulator), /// </summary>
o => o.AsyncPopulator, /// <value>The identifier for the <see cref="AsyncPopulator" /> property.</value>
(o, v) => o.AsyncPopulator = v); public static readonly StyledProperty<Func<string?, CancellationToken, Task<IEnumerable<object>>>?> AsyncPopulatorProperty =
AvaloniaProperty.Register<AutoCompleteBox, Func<string?, CancellationToken, Task<IEnumerable<object>>>?>(
nameof(AsyncPopulator));
/// <summary> /// <summary>
/// Gets or sets the minimum number of characters required to be entered /// Gets or sets the minimum number of characters required to be entered
@ -265,8 +255,8 @@ namespace Avalonia.Controls
/// </value> /// </value>
public bool IsDropDownOpen public bool IsDropDownOpen
{ {
get => _isDropDownOpen; get => GetValue(IsDropDownOpenProperty);
set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); set => SetValue(IsDropDownOpenProperty, value);
} }
/// <summary> /// <summary>
@ -303,8 +293,8 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public object? SelectedItem public object? SelectedItem
{ {
get => _selectedItem; get => GetValue(SelectedItemProperty);
set => SetAndRaise(SelectedItemProperty, ref _selectedItem, value); set => SetValue(SelectedItemProperty, value);
} }
/// <summary> /// <summary>
@ -388,8 +378,8 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public AutoCompleteFilterPredicate<object?>? ItemFilter public AutoCompleteFilterPredicate<object?>? ItemFilter
{ {
get => _itemFilter; get => GetValue(ItemFilterProperty);
set => SetAndRaise(ItemFilterProperty, ref _itemFilter, value); set => SetValue(ItemFilterProperty, value);
} }
/// <summary> /// <summary>
@ -406,8 +396,8 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public AutoCompleteFilterPredicate<string?>? TextFilter public AutoCompleteFilterPredicate<string?>? TextFilter
{ {
get => _textFilter; get => GetValue(TextFilterProperty);
set => SetAndRaise(TextFilterProperty, ref _textFilter, value); set => SetValue(TextFilterProperty, value);
} }
/// <summary> /// <summary>
@ -420,8 +410,8 @@ namespace Avalonia.Controls
/// </value> /// </value>
public AutoCompleteSelector<object>? ItemSelector public AutoCompleteSelector<object>? ItemSelector
{ {
get => _itemSelector; get => GetValue(ItemSelectorProperty);
set => SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); set => SetValue(ItemSelectorProperty, value);
} }
/// <summary> /// <summary>
@ -436,14 +426,14 @@ namespace Avalonia.Controls
/// </value> /// </value>
public AutoCompleteSelector<string?>? TextSelector public AutoCompleteSelector<string?>? TextSelector
{ {
get => _textSelector; get => GetValue(TextSelectorProperty);
set => SetAndRaise(TextSelectorProperty, ref _textSelector, value); set => SetValue(TextSelectorProperty, value);
} }
public Func<string?, CancellationToken, Task<IEnumerable<object>>>? AsyncPopulator public Func<string?, CancellationToken, Task<IEnumerable<object>>>? AsyncPopulator
{ {
get => _asyncPopulator; get => GetValue(AsyncPopulatorProperty);
set => SetAndRaise(AsyncPopulatorProperty, ref _asyncPopulator, value); set => SetValue(AsyncPopulatorProperty, value);
} }
/// <summary> /// <summary>
@ -454,8 +444,8 @@ namespace Avalonia.Controls
/// drop-down portion of the <see cref="AutoCompleteBox" /> control.</value> /// drop-down portion of the <see cref="AutoCompleteBox" /> control.</value>
public IEnumerable? Items public IEnumerable? Items
{ {
get => _itemsEnumerable; get => GetValue(ItemsProperty);
set => SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); set => SetValue(ItemsProperty, value);
} }
} }
} }

65
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@ -94,8 +94,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private const string ElementTextBox = "PART_TextBox"; private const string ElementTextBox = "PART_TextBox";
private IEnumerable? _itemsEnumerable;
/// <summary> /// <summary>
/// Gets or sets a local cached copy of the items data. /// Gets or sets a local cached copy of the items data.
/// </summary> /// </summary>
@ -188,24 +186,15 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private IDisposable? _collectionChangeSubscription; private IDisposable? _collectionChangeSubscription;
private Func<string?, CancellationToken, Task<IEnumerable<object>>>? _asyncPopulator;
private CancellationTokenSource? _populationCancellationTokenSource; private CancellationTokenSource? _populationCancellationTokenSource;
private bool _itemTemplateIsFromValueMemberBinding = true; private bool _itemTemplateIsFromValueMemberBinding = true;
private bool _settingItemTemplateFromValueMemberBinding; private bool _settingItemTemplateFromValueMemberBinding;
private object? _selectedItem;
private bool _isDropDownOpen;
private bool _isFocused = false; private bool _isFocused = false;
private string? _searchText = string.Empty; private string? _searchText = string.Empty;
private AutoCompleteFilterPredicate<object?>? _itemFilter;
private AutoCompleteFilterPredicate<string?>? _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith);
private AutoCompleteSelector<object>? _itemSelector;
private AutoCompleteSelector<string?>? _textSelector;
private readonly EventHandler _populateDropDownHandler; private readonly EventHandler _populateDropDownHandler;
/// <summary> /// <summary>
@ -264,7 +253,7 @@ namespace Avalonia.Controls
bool isEnabled = (bool)e.NewValue!; bool isEnabled = (bool)e.NewValue!;
if (!isEnabled) if (!isEnabled)
{ {
IsDropDownOpen = false; SetCurrentValue(IsDropDownOpenProperty, false);
} }
} }
@ -388,7 +377,7 @@ namespace Avalonia.Controls
{ {
// Reset the old value before it was incorrectly written // Reset the old value before it was incorrectly written
_ignorePropertyChange = true; _ignorePropertyChange = true;
SetValue(e.Property, e.OldValue); SetCurrentValue(e.Property, e.OldValue);
throw new InvalidOperationException("Cannot set read-only property SearchText."); throw new InvalidOperationException("Cannot set read-only property SearchText.");
} }
@ -403,7 +392,7 @@ namespace Avalonia.Controls
AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue!; AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue!;
// Sets the filter predicate for the new value // Sets the filter predicate for the new value
TextFilter = AutoCompleteSearch.GetFilter(mode); SetCurrentValue(TextFilterProperty, AutoCompleteSearch.GetFilter(mode));
} }
/// <summary> /// <summary>
@ -417,12 +406,12 @@ namespace Avalonia.Controls
// If null, revert to the "None" predicate // If null, revert to the "None" predicate
if (value == null) if (value == null)
{ {
FilterMode = AutoCompleteFilterMode.None; SetCurrentValue(FilterModeProperty, AutoCompleteFilterMode.None);
} }
else else
{ {
FilterMode = AutoCompleteFilterMode.Custom; SetCurrentValue(FilterModeProperty, AutoCompleteFilterMode.Custom);
TextFilter = null; SetCurrentValue(TextFilterProperty, null);
} }
} }
@ -442,7 +431,7 @@ namespace Avalonia.Controls
} }
private void OnValueMemberBindingChanged(IBinding? value) private void OnValueMemberBindingChanged(IBinding? value)
{ {
if(_itemTemplateIsFromValueMemberBinding) if (_itemTemplateIsFromValueMemberBinding)
{ {
var template = var template =
new FuncDataTemplate( new FuncDataTemplate(
@ -456,7 +445,7 @@ namespace Avalonia.Controls
}); });
_settingItemTemplateFromValueMemberBinding = true; _settingItemTemplateFromValueMemberBinding = true;
ItemTemplate = template; SetCurrentValue(ItemTemplateProperty, template);
_settingItemTemplateFromValueMemberBinding = false; _settingItemTemplateFromValueMemberBinding = false;
} }
} }
@ -713,7 +702,7 @@ namespace Avalonia.Controls
// The drop down is not open, the Down key will toggle it open. // The drop down is not open, the Down key will toggle it open.
if (e.Key == Key.Down) if (e.Key == Key.Down)
{ {
IsDropDownOpen = true; SetCurrentValue(IsDropDownOpenProperty, true);
e.Handled = true; e.Handled = true;
} }
} }
@ -722,7 +711,7 @@ namespace Avalonia.Controls
switch (e.Key) switch (e.Key)
{ {
case Key.F4: case Key.F4:
IsDropDownOpen = !IsDropDownOpen; SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
e.Handled = true; e.Handled = true;
break; break;
@ -827,7 +816,7 @@ namespace Avalonia.Controls
} }
else else
{ {
IsDropDownOpen = false; SetCurrentValue(IsDropDownOpenProperty, false);
_userCalledPopulate = false; _userCalledPopulate = false;
ClearTextBoxSelection(); ClearTextBoxSelection();
} }
@ -1021,7 +1010,7 @@ namespace Avalonia.Controls
if (args.Cancel) if (args.Cancel)
{ {
_ignorePropertyChange = true; _ignorePropertyChange = true;
SetValue(IsDropDownOpenProperty, oldValue); SetCurrentValue(IsDropDownOpenProperty, oldValue);
} }
else else
{ {
@ -1046,7 +1035,7 @@ namespace Avalonia.Controls
if (args.Cancel) if (args.Cancel)
{ {
_ignorePropertyChange = true; _ignorePropertyChange = true;
SetValue(IsDropDownOpenProperty, oldValue); SetCurrentValue(IsDropDownOpenProperty, oldValue);
} }
else else
{ {
@ -1066,7 +1055,7 @@ namespace Avalonia.Controls
// Force the drop down dependency property to be false. // Force the drop down dependency property to be false.
if (IsDropDownOpen) if (IsDropDownOpen)
{ {
IsDropDownOpen = false; SetCurrentValue(IsDropDownOpenProperty, false);
} }
// Fire the DropDownClosed event // Fire the DropDownClosed event
@ -1088,7 +1077,7 @@ namespace Avalonia.Controls
// Update the prefix/search text. // Update the prefix/search text.
SearchText = Text; SearchText = Text;
if(TryPopulateAsync(SearchText)) if (TryPopulateAsync(SearchText))
{ {
return; return;
} }
@ -1110,7 +1099,7 @@ namespace Avalonia.Controls
_populationCancellationTokenSource?.Dispose(); _populationCancellationTokenSource?.Dispose();
_populationCancellationTokenSource = null; _populationCancellationTokenSource = null;
if(_asyncPopulator == null) if (AsyncPopulator == null)
{ {
return false; return false;
} }
@ -1127,7 +1116,7 @@ namespace Avalonia.Controls
try try
{ {
IEnumerable<object> result = await _asyncPopulator!.Invoke(searchText, cancellationToken); IEnumerable<object> result = await AsyncPopulator!.Invoke(searchText, cancellationToken);
var resultList = result.ToList(); var resultList = result.ToList();
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
@ -1139,7 +1128,7 @@ namespace Avalonia.Controls
{ {
if (!cancellationToken.IsCancellationRequested) if (!cancellationToken.IsCancellationRequested)
{ {
Items = resultList; SetCurrentValue(ItemsProperty, resultList);
PopulateComplete(); PopulateComplete();
} }
}); });
@ -1199,7 +1188,7 @@ namespace Avalonia.Controls
private string? FormatValue(object? value, bool clearDataContext) private string? FormatValue(object? value, bool clearDataContext)
{ {
string? result = FormatValue(value); string? result = FormatValue(value);
if(clearDataContext && _valueBindingEvaluator != null) if (clearDataContext && _valueBindingEvaluator != null)
{ {
_valueBindingEvaluator.ClearDataContext(); _valueBindingEvaluator.ClearDataContext();
} }
@ -1332,7 +1321,7 @@ namespace Avalonia.Controls
// 1. Minimum prefix length // 1. Minimum prefix length
// 2. If a delay timer is in use, use it // 2. If a delay timer is in use, use it
bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0; bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0;
if(populateReady && MinimumPrefixLength == 0 && String.IsNullOrEmpty(newText) && String.IsNullOrEmpty(SearchText)) if (populateReady && MinimumPrefixLength == 0 && String.IsNullOrEmpty(newText) && String.IsNullOrEmpty(SearchText))
{ {
populateReady = false; populateReady = false;
} }
@ -1361,10 +1350,12 @@ namespace Avalonia.Controls
{ {
_skipSelectedItemTextUpdate = true; _skipSelectedItemTextUpdate = true;
} }
SelectedItem = null;
SetCurrentValue(SelectedItemProperty, null);
if (IsDropDownOpen) if (IsDropDownOpen)
{ {
IsDropDownOpen = false; SetCurrentValue(IsDropDownOpenProperty, false);
} }
} }
} }
@ -1600,7 +1591,7 @@ namespace Avalonia.Controls
if (isDropDownOpen != IsDropDownOpen) if (isDropDownOpen != IsDropDownOpen)
{ {
_ignorePropertyChange = true; _ignorePropertyChange = true;
IsDropDownOpen = isDropDownOpen; SetCurrentValue(IsDropDownOpenProperty, isDropDownOpen);
} }
if (IsDropDownOpen) if (IsDropDownOpen)
{ {
@ -1688,7 +1679,7 @@ namespace Avalonia.Controls
{ {
_skipSelectedItemTextUpdate = true; _skipSelectedItemTextUpdate = true;
} }
SelectedItem = newSelectedItem; SetCurrentValue(SelectedItemProperty, newSelectedItem);
// Restore updates for TextSelection // Restore updates for TextSelection
if (_ignoreTextSelectionChange) if (_ignoreTextSelectionChange)
@ -1784,7 +1775,7 @@ namespace Avalonia.Controls
/// <param name="e">The selection changed event data.</param> /// <param name="e">The selection changed event data.</param>
private void OnAdapterSelectionChanged(object? sender, SelectionChangedEventArgs e) private void OnAdapterSelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
SelectedItem = _adapter!.SelectedItem; SetCurrentValue(SelectedItemProperty, _adapter!.SelectedItem);
} }
//TODO Check UpdateTextCompletion //TODO Check UpdateTextCompletion
@ -1795,7 +1786,7 @@ namespace Avalonia.Controls
/// <param name="e">The event data.</param> /// <param name="e">The event data.</param>
private void OnAdapterSelectionComplete(object? sender, RoutedEventArgs e) private void OnAdapterSelectionComplete(object? sender, RoutedEventArgs e)
{ {
IsDropDownOpen = false; SetCurrentValue(IsDropDownOpenProperty, false);
// Completion will update the selected value // Completion will update the selected value
//UpdateTextCompletion(false); //UpdateTextCompletion(false);

92
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

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

109
src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs

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

2
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -49,7 +49,7 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<ScrollBar, Orientation>(nameof(Orientation), Orientation.Vertical); AvaloniaProperty.Register<ScrollBar, Orientation>(nameof(Orientation), Orientation.Vertical);
/// <summary> /// <summary>
/// Defines the <see cref="IsExpandedProperty"/> property. /// Defines the <see cref="IsExpanded"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollBar, bool> IsExpandedProperty = public static readonly DirectProperty<ScrollBar, bool> IsExpandedProperty =
AvaloniaProperty.RegisterDirect<ScrollBar, bool>( AvaloniaProperty.RegisterDirect<ScrollBar, bool>(

2
src/Avalonia.Controls/ScrollViewer.cs

@ -200,7 +200,7 @@ namespace Avalonia.Controls
ScrollBarVisibility.Auto); ScrollBarVisibility.Auto);
/// <summary> /// <summary>
/// Defines the <see cref="IsExpandedProperty"/> property. /// Defines the <see cref="IsExpanded"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ScrollViewer, bool> IsExpandedProperty = public static readonly DirectProperty<ScrollViewer, bool> IsExpandedProperty =
ScrollBar.IsExpandedProperty.AddOwner<ScrollViewer>(o => o.IsExpanded); ScrollBar.IsExpandedProperty.AddOwner<ScrollViewer>(o => o.IsExpanded);

10
src/Avalonia.Controls/Slider.cs

@ -80,16 +80,16 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Slider, TickPlacement>(nameof(TickPlacement), 0d); AvaloniaProperty.Register<Slider, TickPlacement>(nameof(TickPlacement), 0d);
/// <summary> /// <summary>
/// Defines the <see cref="TicksProperty"/> property. /// Defines the <see cref="Ticks"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<AvaloniaList<double>?> TicksProperty = public static readonly StyledProperty<AvaloniaList<double>?> TicksProperty =
TickBar.TicksProperty.AddOwner<Slider>(); TickBar.TicksProperty.AddOwner<Slider>();
// Slider required parts // Slider required parts
private bool _isDragging; protected bool _isDragging;
private Track? _track; protected Track? _track;
private Button? _decreaseButton; protected Button? _decreaseButton;
private Button? _increaseButton; protected Button? _increaseButton;
private IDisposable? _decreaseButtonPressDispose; private IDisposable? _decreaseButtonPressDispose;
private IDisposable? _decreaseButtonReleaseDispose; private IDisposable? _decreaseButtonReleaseDispose;
private IDisposable? _increaseButtonSubscription; private IDisposable? _increaseButtonSubscription;

230
src/Avalonia.Controls/SplitView/SplitView.cs

@ -8,6 +8,7 @@ using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Reactive;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -15,18 +16,21 @@ namespace Avalonia.Controls
/// A control with two views: A collapsible pane and an area for content /// A control with two views: A collapsible pane and an area for content
/// </summary> /// </summary>
[TemplatePart("PART_PaneRoot", typeof(Panel))] [TemplatePart("PART_PaneRoot", typeof(Panel))]
[PseudoClasses(":open", ":closed")] [PseudoClasses(pcOpen, pcClosed)]
[PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] [PseudoClasses(pcCompactOverlay, pcCompactInline, pcOverlay, pcInline)]
[PseudoClasses(":left", ":right")] [PseudoClasses(pcLeft, pcRight)]
[PseudoClasses(":lightdismiss")] [PseudoClasses(pcLightDismiss)]
public class SplitView : ContentControl public class SplitView : ContentControl
{ {
/* protected const string pcOpen = ":open";
Pseudo classes & combos protected const string pcClosed = ":closed";
:open / :closed protected const string pcCompactOverlay = ":compactoverlay";
:compactoverlay :compactinline :overlay :inline protected const string pcCompactInline = ":compactinline";
:left :right protected const string pcOverlay = ":overlay";
*/ protected const string pcInline = ":inline";
protected const string pcLeft = ":left";
protected const string pcRight = ":right";
protected const string pcLightDismiss = ":lightDismiss";
/// <summary> /// <summary>
/// Defines the <see cref="CompactPaneLength"/> property /// Defines the <see cref="CompactPaneLength"/> property
@ -94,8 +98,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="TemplateSettings"/> property /// Defines the <see cref="TemplateSettings"/> property
/// </summary> /// </summary>
public static readonly StyledProperty<SplitViewTemplateSettings> TemplateSettingsProperty = public static readonly DirectProperty<SplitView, SplitViewTemplateSettings> TemplateSettingsProperty =
AvaloniaProperty.Register<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings)); AvaloniaProperty.RegisterDirect<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings),
x => x.TemplateSettings);
/// <summary> /// <summary>
/// Defines the <see cref="PaneClosed"/> event. /// Defines the <see cref="PaneClosed"/> event.
@ -131,18 +136,9 @@ namespace Avalonia.Controls
private Panel? _pane; private Panel? _pane;
private IDisposable? _pointerDisposable; private IDisposable? _pointerDisposable;
private SplitViewTemplateSettings _templateSettings = new SplitViewTemplateSettings();
public SplitView() private string? _lastDisplayModePseudoclass;
{ private string? _lastPlacementPseudoclass;
PseudoClasses.Add(":overlay");
PseudoClasses.Add(":left");
TemplateSettings = new SplitViewTemplateSettings();
}
static SplitView()
{
}
/// <summary> /// <summary>
/// Gets or sets the length of the pane when in <see cref="SplitViewDisplayMode.CompactOverlay"/> /// Gets or sets the length of the pane when in <see cref="SplitViewDisplayMode.CompactOverlay"/>
@ -208,7 +204,7 @@ namespace Avalonia.Controls
get => GetValue(PaneProperty); get => GetValue(PaneProperty);
set => SetValue(PaneProperty, value); set => SetValue(PaneProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the data template used to display the header content of the control. /// Gets or sets the data template used to display the header content of the control.
/// </summary> /// </summary>
@ -235,8 +231,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public SplitViewTemplateSettings TemplateSettings public SplitViewTemplateSettings TemplateSettings
{ {
get => GetValue(TemplateSettingsProperty); get => _templateSettings;
set => SetValue(TemplateSettingsProperty, value); private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value);
} }
/// <summary> /// <summary>
@ -291,7 +287,7 @@ namespace Avalonia.Controls
{ {
return true; return true;
} }
return result; return result;
} }
@ -299,17 +295,18 @@ namespace Avalonia.Controls
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
_pane = e.NameScope.Find<Panel>("PART_PaneRoot"); _pane = e.NameScope.Find<Panel>("PART_PaneRoot");
UpdateVisualStateForDisplayMode(DisplayMode);
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
var topLevel = this.VisualRoot; // :left and :right style triggers contain the template so we need to do this as
if (topLevel is Window window) // soon as we're attached so the template applies. The other visual states can
{ // be updated after the template applies
_pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); UpdateVisualStateForPanePlacementProperty(PanePlacement);
}
} }
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
@ -325,36 +322,11 @@ namespace Avalonia.Controls
if (change.Property == CompactPaneLengthProperty) if (change.Property == CompactPaneLengthProperty)
{ {
var newLen = change.GetNewValue<double>(); UpdateVisualStateForCompactPaneLength(change.GetNewValue<double>());
var displayMode = DisplayMode;
if (displayMode == SplitViewDisplayMode.CompactInline)
{
TemplateSettings.ClosedPaneWidth = newLen;
}
else if (displayMode == SplitViewDisplayMode.CompactOverlay)
{
TemplateSettings.ClosedPaneWidth = newLen;
TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel);
}
} }
else if (change.Property == DisplayModeProperty) else if (change.Property == DisplayModeProperty)
{ {
var oldState = GetPseudoClass(change.GetOldValue<SplitViewDisplayMode>()); UpdateVisualStateForDisplayMode(change.GetNewValue<SplitViewDisplayMode>());
var newState = GetPseudoClass(change.GetNewValue<SplitViewDisplayMode>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = change.GetNewValue<SplitViewDisplayMode>() switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)),
SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)),
_ => throw new NotImplementedException(),
};
TemplateSettings.ClosedPaneWidth = closedPaneWidth;
TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
} }
else if (change.Property == IsPaneOpenProperty) else if (change.Property == IsPaneOpenProperty)
{ {
@ -362,15 +334,15 @@ namespace Avalonia.Controls
if (isPaneOpen) if (isPaneOpen)
{ {
PseudoClasses.Add(":open"); PseudoClasses.Add(pcOpen);
PseudoClasses.Remove(":closed"); PseudoClasses.Remove(pcClosed);
OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this)); OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this));
} }
else else
{ {
PseudoClasses.Add(":closed"); PseudoClasses.Add(pcClosed);
PseudoClasses.Remove(":open"); PseudoClasses.Remove(pcOpen);
OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this)); OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this));
} }
@ -389,33 +361,37 @@ namespace Avalonia.Controls
} }
else if (change.Property == PanePlacementProperty) else if (change.Property == PanePlacementProperty)
{ {
var oldState = GetPseudoClass(change.GetOldValue<SplitViewPanePlacement>()); UpdateVisualStateForPanePlacementProperty(change.GetNewValue<SplitViewPanePlacement>());
var newState = GetPseudoClass(change.GetNewValue<SplitViewPanePlacement>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
} }
else if (change.Property == UseLightDismissOverlayModeProperty) else if (change.Property == UseLightDismissOverlayModeProperty)
{ {
var mode = change.GetNewValue<bool>(); var mode = change.GetNewValue<bool>();
PseudoClasses.Set(":lightdismiss", mode); PseudoClasses.Set(pcLightDismiss, mode);
} }
} }
private void PointerPressedOutside(object? sender, PointerPressedEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
{ {
if (!IsPaneOpen) if (!e.Handled && e.Key == Key.Escape)
{ {
return; if (IsPaneOpen && IsInOverlayMode())
{
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
} }
//If we click within the Pane, don't do anything base.OnKeyDown(e);
//Otherwise, ClosePane if open & using an overlay display mode }
bool closePane = ShouldClosePane();
if (!closePane) private void PointerReleasedOutside(object? sender, PointerReleasedEventArgs e)
{
if (!IsPaneOpen || _pane == null)
{ {
return; return;
} }
var closePane = true;
var src = e.Source as Visual; var src = e.Source as Visual;
while (src != null) while (src != null)
{ {
@ -432,14 +408,15 @@ namespace Avalonia.Controls
src = src.VisualParent; src = src.VisualParent;
} }
if (closePane) if (closePane)
{ {
SetCurrentValue(IsPaneOpenProperty, false); SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true; e.Handled = true;
} }
} }
private bool ShouldClosePane() private bool IsInOverlayMode()
{ {
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);
} }
@ -451,6 +428,7 @@ namespace Avalonia.Controls
protected virtual void OnPaneOpened(RoutedEventArgs args) protected virtual void OnPaneOpened(RoutedEventArgs args)
{ {
EnableLightDismiss();
RaiseEvent(args); RaiseEvent(args);
} }
@ -461,6 +439,8 @@ namespace Avalonia.Controls
protected virtual void OnPaneClosed(RoutedEventArgs args) protected virtual void OnPaneClosed(RoutedEventArgs args)
{ {
_pointerDisposable?.Dispose();
_pointerDisposable = null;
RaiseEvent(args); RaiseEvent(args);
} }
@ -471,14 +451,14 @@ namespace Avalonia.Controls
{ {
return mode switch return mode switch
{ {
SplitViewDisplayMode.Inline => "inline", SplitViewDisplayMode.Inline => pcInline,
SplitViewDisplayMode.CompactInline => "compactinline", SplitViewDisplayMode.CompactInline => pcCompactInline,
SplitViewDisplayMode.Overlay => "overlay", SplitViewDisplayMode.Overlay => pcOverlay,
SplitViewDisplayMode.CompactOverlay => "compactoverlay", SplitViewDisplayMode.CompactOverlay => pcCompactOverlay,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
}; };
} }
/// <summary> /// <summary>
/// Gets the appropriate PseudoClass for the given <see cref="SplitViewPanePlacement"/>. /// Gets the appropriate PseudoClass for the given <see cref="SplitViewPanePlacement"/>.
/// </summary> /// </summary>
@ -486,8 +466,8 @@ namespace Avalonia.Controls
{ {
return placement switch return placement switch
{ {
SplitViewPanePlacement.Left => "left", SplitViewPanePlacement.Left => pcLeft,
SplitViewPanePlacement.Right => "right", SplitViewPanePlacement.Right => pcRight,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null) _ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
}; };
} }
@ -519,6 +499,86 @@ namespace Avalonia.Controls
return value; return value;
} }
private void UpdateVisualStateForCompactPaneLength(double newLen)
{
var displayMode = DisplayMode;
if (displayMode == SplitViewDisplayMode.CompactInline)
{
TemplateSettings.ClosedPaneWidth = newLen;
}
else if (displayMode == SplitViewDisplayMode.CompactOverlay)
{
TemplateSettings.ClosedPaneWidth = newLen;
TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel);
}
}
private void UpdateVisualStateForDisplayMode(SplitViewDisplayMode newValue)
{
if (!string.IsNullOrEmpty(_lastDisplayModePseudoclass))
{
PseudoClasses.Remove(_lastDisplayModePseudoclass);
}
_lastDisplayModePseudoclass = GetPseudoClass(newValue);
PseudoClasses.Add(_lastDisplayModePseudoclass);
var (closedPaneWidth, paneColumnGridLength) = newValue switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)),
SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)),
_ => throw new NotImplementedException(),
};
TemplateSettings.ClosedPaneWidth = closedPaneWidth;
TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
}
private void UpdateVisualStateForPanePlacementProperty(SplitViewPanePlacement newValue)
{
if (!string.IsNullOrEmpty(_lastPlacementPseudoclass))
{
PseudoClasses.Remove(_lastPlacementPseudoclass);
}
_lastPlacementPseudoclass = GetPseudoClass(newValue);
PseudoClasses.Add(_lastPlacementPseudoclass);
}
private void EnableLightDismiss()
{
if (_pane == null)
return;
// If this returns false, we're not in Overlay or CompactOverlay DisplayMode
// and don't need the light dismiss behavior
if (!IsInOverlayMode())
return;
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
{
_pointerDisposable = Disposable.Create(() =>
{
topLevel.PointerReleased -= PointerReleasedOutside;
topLevel.BackRequested -= TopLevelBackRequested;
});
topLevel.PointerReleased += PointerReleasedOutside;
topLevel.BackRequested += TopLevelBackRequested;
}
}
private void TopLevelBackRequested(object? sender, RoutedEventArgs e)
{
if (!IsInOverlayMode())
return;
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
/// <summary> /// <summary>
/// Coerces/validates the <see cref="IsPaneOpen"/> property value. /// Coerces/validates the <see cref="IsPaneOpen"/> property value.
/// </summary> /// </summary>

2
src/Avalonia.Controls/TopLevel.cs

@ -75,7 +75,7 @@ namespace Avalonia.Controls
unsetValue: WindowTransparencyLevel.None); unsetValue: WindowTransparencyLevel.None);
/// <summary> /// <summary>
/// Defines the <see cref="TransparencyBackgroundFallbackProperty"/> property. /// Defines the <see cref="TransparencyBackgroundFallback"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty = public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White); AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);

4
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

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

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Bold.otf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Bold.ttf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.otf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-ExtraLight.ttf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Light.otf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Light.ttf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Medium.otf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Medium.ttf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Regular.otf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.otf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-SemiBold.ttf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Thin.otf

Binary file not shown.

BIN
src/Avalonia.Fonts.Inter/Assets/Inter-Thin.ttf

Binary file not shown.

2
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

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

86
src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs

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

5
src/Avalonia.Native/AvaloniaNativePlatform.cs

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

13
src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs

@ -60,7 +60,18 @@ namespace Avalonia.Native
return; return;
} }
_inputMethod.SetCursorRect(_client.CursorRectangle.ToAvnRect()); var visualRoot = _client.TextViewVisual.VisualRoot;
var transform = _client.TextViewVisual.TransformToVisual((Visual)visualRoot);
if (transform == null)
{
return;
}
var rect = _client.CursorRectangle.TransformToAABB(transform.Value);
_inputMethod.SetCursorRect(rect.ToAvnRect());
} }
private void OnSurroundingTextChanged(object sender, EventArgs e) private void OnSurroundingTextChanged(object sender, EventArgs e)

7
src/Avalonia.Native/CallbackBase.cs

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

132
src/Avalonia.Native/DispatcherImpl.cs

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

115
src/Avalonia.Native/PlatformThreadingInterface.cs

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

16
src/Avalonia.Native/avn.idl

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

4
src/Avalonia.X11/X11Platform.cs

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

159
src/Avalonia.X11/X11PlatformThreading.cs

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

3
src/Avalonia.X11/X11Window.cs

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

5
src/Browser/Avalonia.Browser/WindowingPlatform.cs

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

9
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

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

11
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs

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

4
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs

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

12
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

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

109
src/Shared/RawEventGrouping.cs

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

2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -72,7 +72,7 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (textSpan[i] == '\t') if (i < textSpan.Length && textSpan[i] == '\t')
{ {
glyphIndex = typeface.GetGlyph(' '); glyphIndex = typeface.GetGlyph(' ');

2
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -68,7 +68,7 @@ namespace Avalonia.Direct2D1.Media
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (textSpan[i] == '\t') if (i < textSpan.Length && textSpan[i] == '\t')
{ {
glyphIndex = typeface.GetGlyph(' '); glyphIndex = typeface.GetGlyph(' ');

43
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

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

121
src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs

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

99
src/Windows/Avalonia.Win32/Win32Platform.cs

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

5
src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs

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

20
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

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

6
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

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

148
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Reactive.Concurrency;
using System.Reactive.Subjects;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
@ -351,6 +353,134 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("inheriteddefault", target.Inherited); Assert.Equal("inheriteddefault", target.Inherited);
} }
[Fact]
public void SetCurrent_Value_Persists_When_Toggling_Style_3()
{
var target = new Class1();
var root = new TestRoot(target)
{
Styles =
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.BarProperty, "bar"),
new Setter(Class1.InheritedProperty, "inherited"),
},
}
}
};
root.LayoutManager.ExecuteInitialLayoutPass();
target.SetValue(Class1.FooProperty, "not current", BindingPriority.Template);
target.SetCurrentValue(Class1.FooProperty, "current");
Assert.Equal("current", target.Foo);
Assert.Equal("bardefault", target.Bar);
Assert.Equal("inheriteddefault", target.Inherited);
target.Classes.Add("foo");
Assert.Equal("current", target.Foo);
Assert.Equal("bar", target.Bar);
Assert.Equal("inherited", target.Inherited);
target.Classes.Remove("foo");
Assert.Equal("current", target.Foo);
Assert.Equal("bardefault", target.Bar);
Assert.Equal("inheriteddefault", target.Inherited);
}
[Theory]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.Style)]
[InlineData(BindingPriority.Animation)]
public void CurrentValue_Is_Replaced_By_Binding_Value(BindingPriority priority)
{
var target = new Class1();
var source = new BehaviorSubject<string>("initial");
target.Bind(Class1.FooProperty, source, priority);
target.SetCurrentValue(Class1.FooProperty, "current");
source.OnNext("new");
Assert.Equal("new", target.Foo);
}
[Fact]
public void CurrentValue_Is_Replaced_By_New_Style_Activation_1()
{
var target = new Class1();
var root = new TestRoot(target)
{
Styles =
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.FooProperty, "initial"),
new Setter(Class1.BarProperty, "bar"),
},
},
new Style(x => x.OfType<Class1>().Class("bar"))
{
Setters =
{
new Setter(Class1.FooProperty, "new"),
new Setter(Class1.BarProperty, "baz"),
},
}, }
};
root.LayoutManager.ExecuteInitialLayoutPass();
target.Classes.Add("foo");
Assert.Equal("initial", target.Foo);
target.SetCurrentValue(Class1.FooProperty, "current");
target.Classes.Add("bar");
Assert.Equal("new", target.Foo);
}
[Fact]
public void CurrentValue_Is_Replaced_By_New_Style_Activation_2()
{
var target = new Class1();
var root = new TestRoot(target)
{
Styles =
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.FooProperty, "foo"),
},
},
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.BarProperty, "bar"),
},
},
}
};
root.LayoutManager.ExecuteInitialLayoutPass();
target.SetValue(Class1.FooProperty, "template", BindingPriority.Template);
target.SetCurrentValue(Class1.FooProperty, "current");
target.Classes.Add("foo");
Assert.Equal("foo", target.Foo);
}
private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property)
{ {
return target.GetDiagnostic(property).Priority; return target.GetDiagnostic(property).Priority;
@ -383,5 +513,23 @@ namespace Avalonia.Base.UnitTests
return Math.Min(value, ((Class1)sender).CoerceMax); return Math.Min(value, ((Class1)sender).CoerceMax);
} }
} }
private class ViewModel : NotifyingBase
{
private string _value;
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
RaisePropertyChanged();
}
}
}
}
} }
} }

48
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs

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

13
tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs

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

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save