Browse Source

Merge branch 'master' into markup-nullable

pull/10608/head
Steven Kirk 3 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\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",

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

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

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

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

35
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -28,6 +28,41 @@
<Grid Grid.Column="2"
Grid.Row="0"
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"
Grid.Row="0"
Color="Red"

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

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

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

@ -125,7 +125,7 @@ namespace Avalonia.Layout
AvaloniaProperty.Register<Layoutable, VerticalAlignment>(nameof(VerticalAlignment));
/// <summary>
/// Defines the <see cref="UseLayoutRoundingProperty"/> property.
/// Defines the <see cref="UseLayoutRounding"/> property.
/// </summary>
public static readonly StyledProperty<bool> UseLayoutRoundingProperty =
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)
{
var extendedLength = 2 * input.Length;
#if !BUILDTASK
Span<char> extended = stackalloc char[extendedLength];
#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(familyName,
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
@ -135,7 +133,7 @@ namespace Avalonia.Media
foreach (var familyName in fontFamily.FamilyNames)
{
if (SystemFonts.TryGetGlyphTypeface(familyName.ToUpperInvariant(), typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
}

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

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

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

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

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

@ -131,7 +131,7 @@ namespace Avalonia.Media
/// </summary>
/// <remarks>
/// <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>
/// </list>
/// </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 unsafe struct Rgb24PixelFormatReader : IPixelFormatReader
{
private byte* _address;
public Rgba8888Pixel ReadNext()
{
var addr = _address;
_address += 3;
return new Rgba8888Pixel
{
R = addr[0],
G = addr[1],
B = addr[2],
A = 255,
};
}
public void Reset(IntPtr address) => _address = (byte*)address;
}
public unsafe struct Bgr24PixelFormatReader : IPixelFormatReader
{
private byte* _address;
public Rgba8888Pixel ReadNext()
{
var addr = _address;
_address += 3;
return new Rgba8888Pixel
{
R = addr[2],
G = addr[1],
B = addr[0],
A = 255,
};
}
public void Reset(IntPtr address) => _address = (byte*)address;
}
public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst,
PixelFormat format)
@ -242,6 +280,10 @@ static unsafe class PixelFormatReader
Transcode<Gray8PixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray16)
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)
Transcode<Gray32FloatPixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Rgba64)
@ -258,7 +300,9 @@ static unsafe class PixelFormatReader
|| format == PixelFormats.Gray8
|| format == PixelFormats.Gray16
|| format == PixelFormats.Gray32Float
|| format == PixelFormats.Rgba64;
|| format == PixelFormats.Rgba64
|| format == PixelFormats.Bgr24
|| format == PixelFormats.Rgb24;
}
public static void Transcode<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]
public interface IPlatformThreadingInterface
{
void RunLoop(CancellationToken cancellationToken);
/// <summary>
/// Starts a timer.
/// </summary>

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

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

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

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

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

@ -395,7 +395,7 @@ namespace Avalonia.PropertyStore
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.BasePriority)
ReevaluateEffectiveValue(property, existing);
ReevaluateEffectiveValue(property, existing, changedValueEntry: entry);
}
else
{
@ -774,6 +774,7 @@ namespace Avalonia.PropertyStore
private void ReevaluateEffectiveValue(
AvaloniaProperty property,
EffectiveValue? current,
IValueEntry? changedValueEntry = null,
bool ignoreLocalValue = false)
{
++_isEvaluating;
@ -796,6 +797,12 @@ namespace Avalonia.PropertyStore
{
var frame = _frames[i];
var priority = frame.Priority;
// Exit early if the current EffectiveValue has higher priority than this frame.
if (current?.Priority < priority && current?.BasePriority < priority)
break;
// Try to get an entry from the frame for the property we're reevaluating.
var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged);
// If the active state of the frame has changed since the last read, and
@ -803,20 +810,17 @@ namespace Avalonia.PropertyStore
// effective values of all properties.
if (activeChanged && frame.EntryCount > 1)
{
ReevaluateEffectiveValues();
ReevaluateEffectiveValues(changedValueEntry);
return;
}
// We're interested in the value if:
// - There is no current effective value, or
// - The value's priority is higher than the current effective value's priority, or
// - The value is a non-animation value and its priority is higher than the current
// effective value's base priority
var isRelevantPriority = current is null ||
(priority < current.Priority && priority < current.BasePriority) ||
(priority > BindingPriority.Animation && priority < current.BasePriority);
if (foundEntry && isRelevantPriority && entry!.HasValue)
// If the frame has an entry for this property with a higher priority than the
// current effective value (and that entry has a value), then we have a new
// value for the property. Note that the check for entry.HasValue must be
// evaluated last as it can cause bindings to be subscribed.
if (foundEntry &&
HasHigherPriority(entry!, priority, current, changedValueEntry) &&
entry!.HasValue)
{
if (current is not null)
{
@ -832,10 +836,6 @@ namespace Avalonia.PropertyStore
if (generation != _frameGeneration)
goto restart;
if (current?.Priority < BindingPriority.Unset &&
current?.BasePriority < BindingPriority.Unset)
break;
}
if (current?.Priority == BindingPriority.Unset)
@ -859,7 +859,7 @@ namespace Avalonia.PropertyStore
}
}
private void ReevaluateEffectiveValues()
private void ReevaluateEffectiveValues(IValueEntry? changedValueEntry = null)
{
++_isEvaluating;
@ -894,10 +894,9 @@ namespace Avalonia.PropertyStore
{
var entry = frame.GetEntry(j);
var property = entry.Property;
// Skip if we already have a value/base value for this property.
if (_effectiveValues.TryGetValue(property, out var effectiveValue) &&
effectiveValue.BasePriority < BindingPriority.Unset)
_effectiveValues.TryGetValue(property, out var effectiveValue);
if (!HasHigherPriority(entry, priority, effectiveValue, changedValueEntry))
continue;
if (!entry.HasValue)
@ -942,6 +941,37 @@ namespace Avalonia.PropertyStore
}
}
private static bool HasHigherPriority(
IValueEntry entry,
BindingPriority entryPriority,
EffectiveValue? current,
IValueEntry? changedValueEntry)
{
// Set the value if: there is no current effective value; or
if (current is null)
return true;
// The value's priority is higher than the current effective value's priority; or
if (entryPriority < current.Priority && entryPriority < current.BasePriority)
return true;
// - The value's priority is equal to the current effective value's priority
// - But the effective value was set via SetCurrentValue
// - As long as the SetCurrentValue wasn't overriding the value from the value entry under consideration
// - Or if it was, the value entry under consideration has changed; or
if (entryPriority == current.Priority &&
current.IsOverridenCurrentValue &&
(current.ValueEntry != entry || entry == changedValueEntry))
return true;
// The value is a non-animation value and its priority is higher than the current effective value's base
// priority.
if (entryPriority > BindingPriority.Animation && entryPriority < current.BasePriority)
return true;
return false;
}
private bool TryGetEffectiveValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value)

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

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

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

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

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

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

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

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

103
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));
/// <summary>
/// Defines the <see cref="IsVisibleProperty"/> property.
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVisibleProperty =
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>
/// 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)
{
// 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.
var scale = LayoutHelper.GetLayoutScale(this);
var pixelWidth = Convert.ToInt32(Bounds.Width * scale);
var pixelHeight = Convert.ToInt32(Bounds.Height * scale);
int pixelWidth;
int pixelHeight;
if (base._track != null)
{
pixelWidth = Convert.ToInt32(base._track.Bounds.Width * scale);
pixelHeight = Convert.ToInt32(base._track.Bounds.Height * scale);
}
else
{
// As a fallback, attempt to calculate using the overall control size
// This shouldn't happen as a track is a required template part of a slider
// However, if it does, the spectrum will still be shown
pixelWidth = Convert.ToInt32(Bounds.Width * scale);
pixelHeight = Convert.ToInt32(Bounds.Height * scale);
}
if (pixelWidth != 0 && pixelHeight != 0)
{
@ -373,7 +387,7 @@ namespace Avalonia.Controls.Primitives
ignorePropertyChanged = true;
// Always keep the two color properties in sync
HsvColor = Color.ToHsv();
SetCurrentValue(HsvColorProperty, Color.ToHsv());
SetColorToSliderValues();
UpdateBackground();
@ -403,7 +417,7 @@ namespace Avalonia.Controls.Primitives
ignorePropertyChanged = true;
// Always keep the two color properties in sync
Color = HsvColor.ToRgb();
SetCurrentValue(ColorProperty, HsvColor.ToRgb());
SetColorToSliderValues();
UpdateBackground();
@ -440,13 +454,13 @@ namespace Avalonia.Controls.Primitives
if (ColorModel == ColorModel.Hsva)
{
HsvColor = hsvColor;
Color = hsvColor.ToRgb();
SetCurrentValue(HsvColorProperty, hsvColor);
SetCurrentValue(ColorProperty, hsvColor.ToRgb());
}
else
{
Color = color;
HsvColor = color.ToHsv();
SetCurrentValue(ColorProperty, color);
SetCurrentValue(HsvColorProperty, color.ToHsv());
}
UpdatePseudoClasses();

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

@ -96,10 +96,10 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="ThirdComponent"/> property.
/// </summary>
public static readonly StyledProperty<ColorComponent> ThirdComponentProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorComponent>(
public static readonly DirectProperty<ColorSpectrum, ColorComponent> ThirdComponentProperty =
AvaloniaProperty.RegisterDirect<ColorSpectrum, ColorComponent>(
nameof(ThirdComponent),
ColorComponent.Component3); // Value
o => o.ThirdComponent);
/// <summary>
/// Gets or sets the currently selected color in the RGB color model.
@ -239,8 +239,8 @@ namespace Avalonia.Controls.Primitives
/// </remarks>
public ColorComponent ThirdComponent
{
get => GetValue(ThirdComponentProperty);
protected set => SetValue(ThirdComponentProperty, value);
get => _thirdComponent;
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.Media;
using Avalonia.Media.Imaging;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.Reactive;
namespace Avalonia.Controls.Primitives
{
@ -48,6 +48,7 @@ namespace Avalonia.Controls.Primitives
private bool _isPointerPressed = false;
private bool _shouldShowLargeSelection = false;
private List<Hsv> _hsvValues = new List<Hsv>();
private ColorComponent _thirdComponent = ColorComponent.Component3; // HsvComponent.Value
private IDisposable? _layoutRootDisposable;
private IDisposable? _selectionEllipsePanelDisposable;
@ -403,7 +404,7 @@ namespace Avalonia.Controls.Primitives
_updatingHsvColor = true;
Hsv newHsv = (new Rgb(color)).ToHsv();
HsvColor = newHsv.ToHsvColor(color.A / 255.0);
SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(color.A / 255.0));
_updatingHsvColor = false;
UpdateEllipse();
@ -534,7 +535,7 @@ namespace Avalonia.Controls.Primitives
_updatingColor = true;
Rgb newRgb = (new Hsv(hsvColor)).ToRgb();
Color = newRgb.ToColor(hsvColor.A);
SetCurrentValue(ColorProperty, newRgb.ToColor(hsvColor.A));
_updatingColor = false;
@ -608,8 +609,8 @@ namespace Avalonia.Controls.Primitives
Rgb newRgb = newHsv.ToRgb();
double alpha = HsvColor.A;
Color = newRgb.ToColor(alpha);
HsvColor = newHsv.ToHsvColor(alpha);
SetCurrentValue(ColorProperty, newRgb.ToColor(alpha));
SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha));
UpdateEllipse();
UpdatePseudoClasses();

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),
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>
/// Defines the <see cref="HsvColor"/> property.
/// </summary>
@ -260,6 +268,16 @@ namespace Avalonia.Controls
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"/>
public HsvColor HsvColor
{

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

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

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

@ -2,6 +2,7 @@
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters
{
@ -10,6 +11,11 @@ namespace Avalonia.Controls.Converters
/// </summary>
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/>
public object? Convert(
object? value,
@ -42,16 +48,7 @@ namespace Avalonia.Controls.Converters
return AvaloniaProperty.UnsetValue;
}
string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant();
if (includeSymbol == false)
{
// TODO: When .net standard 2.0 is dropped, replace the below line
//hexColor = hexColor.Replace("#", string.Empty, StringComparison.Ordinal);
hexColor = hexColor.Replace("#", string.Empty);
}
return hexColor;
return ToHexString(color, AlphaPosition, includeSymbol);
}
/// <inheritdoc/>
@ -62,21 +59,159 @@ namespace Avalonia.Controls.Converters
CultureInfo culture)
{
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;
}
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
return AvaloniaProperty.UnsetValue;
var extendedLength = 2 * input.Length;
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.Globalization;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Utilities;
@ -11,8 +11,11 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public static class ColorHelper
{
private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>();
private static readonly object cacheMutex = new object();
private static readonly Dictionary<HsvColor, string> _cachedDisplayNames = new Dictionary<HsvColor, string>();
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>
/// 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>
public static string ToDisplayName(Color color)
{
// Without rounding, there are 16,777,216 possible RGB colors (without alpha).
var hsvColor = color.ToHsv();
// Handle extremes that are outside the below algorithm
if (color.A == 0x00)
{
return GetDisplayName(KnownColor.Transparent);
}
// HSV ----------------------------------------------------------------------
//
// There are far too many possible HSV colors to cache and search through
// for performance reasons. Therefore, the HSV color is rounded.
// Rounding is tolerable in this algorithm because it is perception based.
// Hue is the most important for user perception so is rounded the least.
// Then there is a lot of loss in rounding the saturation and value components
// which are not as closely related to perceived color.
//
// Hue : Round to nearest int (0..360)
// Saturation : Round to the nearest 1/10 (0..1)
// Value : Round to the nearest 1/10 (0..1)
// Alpha : Is ignored in this algorithm
//
// Rounding results in ~36_000 values to cache in the worse case.
//
// RGB ----------------------------------------------------------------------
//
// The original algorithm worked in RGB color space.
// If this code is every adjusted to work in RGB again note the following:
//
// Without rounding, there are 16_777_216 possible RGB colors (without alpha).
// This is too many to cache and search through for performance reasons.
// It is also needlessly large as there are only ~140 known/named colors.
// Therefore, rounding of the input color's component values is done to
@ -68,42 +100,67 @@ namespace Avalonia.Controls.Primitives
// The rounding value of 5 is specially chosen.
// It is a factor of 255 and therefore evenly divisible which improves
// the quality of the calculations.
double rounding = 5;
var roundedColor = new Color(
0xFF,
Convert.ToByte(Math.Round(color.R / rounding) * rounding),
Convert.ToByte(Math.Round(color.G / rounding) * rounding),
Convert.ToByte(Math.Round(color.B / rounding) * rounding));
var roundedHsvColor = new HsvColor(
1.0,
Math.Round(hsvColor.H, 0),
Math.Round(hsvColor.S, 1),
Math.Round(hsvColor.V, 1));
// Attempt to use a previously cached display name
lock (cacheMutex)
lock (_displayNameCacheMutex)
{
if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName))
if (_cachedDisplayNames.TryGetValue(roundedHsvColor, out var displayName))
{
return displayName;
}
}
// Build the KnownColor name cache if it doesn't already exist
lock (_knownColorCacheMutex)
{
if (_cachedKnownColorNames.Count == 0)
{
for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1
{
KnownColor knownColor = _knownColors[i];
// Some known colors have the same numerical value. For example:
// - Aqua = 0xff00ffff
// - Cyan = 0xff00ffff
//
// This is not possible to represent in a dictionary which requires
// unique values. Therefore, only the first value is used.
if (!_cachedKnownColorNames.ContainsKey(knownColor))
{
_cachedKnownColorNames.Add(knownColor, GetDisplayName(knownColor));
}
}
}
}
// Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
// This is done in HSV color space to most closely match user-perception
var closestKnownColor = KnownColor.None;
var closestKnownColorDistance = double.PositiveInfinity;
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
for (int i = 1; i < knownColors.Length; i++) // Skip 'None'
for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1
{
KnownColor knownColor = _knownColors[i];
// Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColors[i] != KnownColor.Transparent)
if (knownColor != KnownColor.Transparent)
{
Color knownColor = KnownColors.ToColor(knownColors[i]);
HsvColor knownHsvColor = KnownColors.ToColor(knownColor).ToHsv();
double distance = Math.Sqrt(
Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) +
Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) +
Math.Pow((double)(roundedColor.B - knownColor.B), 2.0));
Math.Pow((roundedHsvColor.H - knownHsvColor.H), 2.0) +
Math.Pow((roundedHsvColor.S - knownHsvColor.S), 2.0) +
Math.Pow((roundedHsvColor.V - knownHsvColor.V), 2.0));
if (distance < closestKnownColorDistance)
{
closestKnownColor = knownColors[i];
closestKnownColor = knownColor;
closestKnownColorDistance = distance;
}
}
@ -113,26 +170,19 @@ namespace Avalonia.Controls.Primitives
// Cache results for next time as well
if (closestKnownColor != KnownColor.None)
{
var sb = StringBuilderCache.Acquire();
string name = closestKnownColor.ToString();
string? displayName;
// Add spaces converting PascalCase to human-readable names
for (int i = 0; i < name.Length; i++)
lock (_knownColorCacheMutex)
{
if (i != 0 &&
char.IsUpper(name[i]))
if (!_cachedKnownColorNames.TryGetValue(closestKnownColor, out displayName))
{
sb.Append(' ');
displayName = GetDisplayName(closestKnownColor);
}
sb.Append(name[i]);
}
string displayName = StringBuilderCache.GetStringAndRelease(sb);
lock (cacheMutex)
lock (_displayNameCacheMutex)
{
cachedDisplayNames.Add(roundedColor, displayName);
_cachedDisplayNames.Add(roundedHsvColor, displayName);
}
return displayName;
@ -142,5 +192,35 @@ namespace Avalonia.Controls.Primitives
return string.Empty;
}
}
/// <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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Layout;

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

@ -42,7 +42,8 @@
</Panel>
</DropDownButton.Content>
<DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="nopadding">
<Flyout FlyoutPresenterClasses="nopadding"
Placement="Top">
<!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master).
@ -216,6 +217,7 @@
Content="RGB"
CornerRadius="4,0,0,4"
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}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -223,6 +225,7 @@
Content="HSV"
CornerRadius="0,4,4,0"
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}" />
</Grid>
<Grid x:Name="HexInputGrid"

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

@ -1,13 +1,20 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
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"
TargetType="Thumb">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />-->
<Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
@ -25,27 +32,28 @@
<Style Selector="^:horizontal">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Height" Value="20" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Height" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
Background="{TemplateBinding Background}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track"
Height="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
VerticalAlignment="Center"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
@ -82,7 +90,7 @@
</RepeatButton>
</Track.IncreaseButton>
<Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}"
Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
@ -97,26 +105,27 @@
<Style Selector="^:vertical">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Width" Value="20" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Width" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Border HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Border HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
Background="{TemplateBinding Background}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
Width="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
@ -154,7 +163,7 @@
</RepeatButton>
</Track.IncreaseButton>
<Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}"
Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"

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

@ -8,7 +8,6 @@
<pc:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:ColorToHexConverter x:Key="ColorToHexConverter" />
<converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
<globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
<x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
@ -465,6 +464,7 @@
Content="RGB"
CornerRadius="4,0,0,4"
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}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -472,6 +472,7 @@
Content="HSV"
CornerRadius="0,4,4,0"
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}" />
</Grid>
<Grid x:Name="HexInputGrid"

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

@ -42,7 +42,7 @@
</Panel>
</DropDownButton.Content>
<DropDownButton.Flyout>
<Flyout>
<Flyout Placement="Top">
<!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master).
@ -216,6 +216,7 @@
Content="RGB"
CornerRadius="0,0,0,0"
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}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -223,6 +224,7 @@
Content="HSV"
CornerRadius="0,0,0,0"
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}" />
</Grid>
<Grid x:Name="HexInputGrid"

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

@ -1,13 +1,20 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
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"
TargetType="Thumb">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
<!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />-->
<Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
@ -25,27 +32,28 @@
<Style Selector="^:horizontal">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Height" Value="20" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Height" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
Background="{TemplateBinding Background}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track"
Height="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
VerticalAlignment="Center"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
@ -82,7 +90,7 @@
</RepeatButton>
</Track.IncreaseButton>
<Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}"
Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
@ -97,26 +105,27 @@
<Style Selector="^:vertical">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Width" Value="20" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Width" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Border HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Border HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
Background="{TemplateBinding Background}"
CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
Width="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
@ -154,7 +163,7 @@
</RepeatButton>
</Track.IncreaseButton>
<Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}"
Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"

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

@ -8,7 +8,6 @@
<pc:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:ColorToHexConverter x:Key="ColorToHexConverter" />
<converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
<globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
<x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
@ -427,6 +426,7 @@
Content="RGB"
CornerRadius="0,0,0,0"
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}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -434,6 +434,7 @@
Content="HSV"
CornerRadius="0,0,0,0"
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}" />
</Grid>
<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));
/// <summary>
/// Defines the <see cref="MaximumRowsOrColumnsProperty"/> property.
/// Defines the <see cref="MaximumRowsOrColumns"/> property.
/// </summary>
public static readonly StyledProperty<int> MaximumRowsOrColumnsProperty =
AvaloniaProperty.Register<UniformGridLayout, int>(nameof(MaximumRowsOrColumns));

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

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

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

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

2
src/Avalonia.Controls/ScrollViewer.cs

@ -200,7 +200,7 @@ namespace Avalonia.Controls
ScrollBarVisibility.Auto);
/// <summary>
/// Defines the <see cref="IsExpandedProperty"/> property.
/// Defines the <see cref="IsExpanded"/> property.
/// </summary>
public static readonly DirectProperty<ScrollViewer, bool> IsExpandedProperty =
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);
/// <summary>
/// Defines the <see cref="TicksProperty"/> property.
/// Defines the <see cref="Ticks"/> property.
/// </summary>
public static readonly StyledProperty<AvaloniaList<double>?> TicksProperty =
TickBar.TicksProperty.AddOwner<Slider>();
// Slider required parts
private bool _isDragging;
private Track? _track;
private Button? _decreaseButton;
private Button? _increaseButton;
protected bool _isDragging;
protected Track? _track;
protected Button? _decreaseButton;
protected Button? _increaseButton;
private IDisposable? _decreaseButtonPressDispose;
private IDisposable? _decreaseButtonReleaseDispose;
private IDisposable? _increaseButtonSubscription;

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

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

2
src/Avalonia.Controls/TopLevel.cs

@ -75,7 +75,7 @@ namespace Avalonia.Controls
unsetValue: WindowTransparencyLevel.None);
/// <summary>
/// Defines the <see cref="TransparencyBackgroundFallbackProperty"/> property.
/// Defines the <see cref="TransparencyBackgroundFallback"/> property.
/// </summary>
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
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.Remote.Protocol;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
@ -46,13 +47,12 @@ namespace Avalonia.DesignerSupport.Remote
{
s_transport = transport;
var instance = new PreviewerWindowingPlatform();
var threading = new InternalPlatformThreadingInterface();
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardStub>()
.Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IWindowingPlatform>().ToConstant(instance)

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)
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface())
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
.Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()

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

13
src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs

@ -60,7 +60,18 @@ namespace Avalonia.Native
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)

7
src/Avalonia.Native/CallbackBase.cs

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

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

4
src/Avalonia.X11/X11Platform.cs

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

159
src/Avalonia.X11/X11PlatformThreading.cs

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

3
src/Avalonia.X11/X11Window.cs

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

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

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

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

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

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

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

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

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

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

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

109
src/Shared/RawEventGrouping.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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]
[Theory]
public void GenericCheck(AnimationData data)
{
var compositor =
new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer(), new Dispatcher(null)), null);
new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer(), new DummyDispatcher()), null);
var target = compositor.CreateSolidColorVisual();
var ani = new ScalarKeyFrameAnimation(null);
foreach (var frame in data.Frames)

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

Loading…
Cancel
Save