From 7311ed01b68ba942f2f5929550630d71542ebae8 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 22 Feb 2023 20:14:57 -0500 Subject: [PATCH 01/58] Use SetCurrentValue() in ColorPicker controls --- .../ColorPreviewer/ColorPreviewer.cs | 2 +- .../ColorSlider/ColorSlider.cs | 12 ++++++------ .../ColorSpectrum/ColorSpectrum.cs | 14 +++++++------- .../ColorView/ColorView.cs | 14 +++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 6f49430505..58702ecb61 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives if (accentStep != 0) { // ColorChanged will be invoked in OnPropertyChanged if the value is different - HsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); + SetCurrentValue(HsvColorProperty, AccentColorConverter.GetAccent(hsvColor, accentStep)); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index dd5e7d5b01..7cbadcdf6d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -373,7 +373,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 +403,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 +440,13 @@ namespace Avalonia.Controls.Primitives if (ColorModel == ColorModel.Hsva) { - HsvColor = hsvColor; - Color = hsvColor.ToRgb(); + SetCurrentValue(HsvColorProperty, hsvColor); + SetCurrentValue(ColorProperty, hsvColor.ToRgb()); } else { - Color = color; - HsvColor = color.ToHsv(); + SetCurrentValue(ColorProperty, color); + SetCurrentValue(HsvColorProperty, color.ToHsv()); } UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 6f4c0003a8..fde011bc46 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -403,7 +403,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(); @@ -509,15 +509,15 @@ namespace Avalonia.Controls.Primitives { case ColorSpectrumComponents.HueSaturation: case ColorSpectrumComponents.SaturationHue: - ThirdComponent = (ColorComponent)HsvComponent.Value; + SetCurrentValue(ThirdComponentProperty, (ColorComponent)HsvComponent.Value); break; case ColorSpectrumComponents.HueValue: case ColorSpectrumComponents.ValueHue: - ThirdComponent = (ColorComponent)HsvComponent.Saturation; + SetCurrentValue(ThirdComponentProperty, (ColorComponent)HsvComponent.Saturation); break; case ColorSpectrumComponents.SaturationValue: case ColorSpectrumComponents.ValueSaturation: - ThirdComponent = (ColorComponent)HsvComponent.Hue; + SetCurrentValue(ThirdComponentProperty, (ColorComponent)HsvComponent.Hue); break; } @@ -534,7 +534,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 +608,8 @@ namespace Avalonia.Controls.Primitives Rgb newRgb = newHsv.ToRgb(); double alpha = HsvColor.A; - Color = newRgb.ToColor(alpha); - HsvColor = newHsv.ToHsvColor(alpha); + SetCurrentValue(ColorProperty, newRgb.ToColor(alpha)); + SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha)); UpdateEllipse(); UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 977b1f5c84..38be6cfc5b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -50,7 +50,7 @@ namespace Avalonia.Controls if (convertedColor is Color color) { - Color = color; + SetCurrentValue(ColorProperty, color); } // Re-apply the hex value @@ -167,7 +167,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; @@ -211,7 +211,7 @@ namespace Avalonia.Controls { ignorePropertyChanged = true; - HsvColor = Color.ToHsv(); + SetCurrentValue(HsvColorProperty, Color.ToHsv()); SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( @@ -224,7 +224,7 @@ namespace Avalonia.Controls { ignorePropertyChanged = true; - Color = HsvColor.ToRgb(); + SetCurrentValue(ColorProperty, HsvColor.ToRgb()); SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( @@ -241,7 +241,7 @@ namespace Avalonia.Controls // bound properties controlling the palette grid if (palette != null) { - PaletteColumnCount = palette.ColorCount; + SetCurrentValue(PaletteColumnCountProperty, palette.ColorCount); List newPaletteColors = new List(); for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) @@ -252,14 +252,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 || From 76e0e54a0008ae81cd3a9daf36c7a89568a9c323 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 7 Mar 2023 23:46:39 -0500 Subject: [PATCH 02/58] Restore ColorPicker flyout top placement Default behavior was changed after https://github.com/AvaloniaUI/Avalonia/pull/10492 --- .../Themes/Fluent/ColorPicker.xaml | 3 ++- .../Themes/Simple/ColorPicker.xaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 8f52b059f1..e6b25fa387 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -42,7 +42,8 @@ - + diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 5d83ced011..089f5cb660 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -4,11 +4,8 @@ using Avalonia.Platform; namespace Avalonia.Threading; -interface IDispatcherImpl +public interface IDispatcherImpl { - - //IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick); - bool CurrentThreadIsLoopThread { get; } // Asynchronously triggers Signaled callback @@ -19,7 +16,7 @@ interface IDispatcherImpl } -interface IDispatcherImplWithPendingInput : IDispatcherImpl +public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can bool CanQueryPendingInput { get; } @@ -27,7 +24,7 @@ interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } -interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput +public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop void RunLoop(CancellationToken token); diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs deleted file mode 100644 index e1f6db9c60..0000000000 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ /dev/null @@ -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? Signaled; -#pragma warning disable CS0067 - public event Action? Tick; -#pragma warning restore CS0067 - - } -} diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs new file mode 100644 index 0000000000..bc7595ef4e --- /dev/null +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -0,0 +1,108 @@ +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 void UpdateTimer(int? dueTimeInTicks) + { + lock (_lock) + { + _nextTimer = dueTimeInTicks == null + ? null + : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInTicks.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(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index cd14b2d69a..808153cc4a 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/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().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToSingleton() - .Bind().ToConstant(threading) + .Bind().ToConstant(new ManagedDispatcherImpl(null)) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(instance) diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b0b1d731d2..319b0da7bf 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -60,7 +60,7 @@ namespace Avalonia.Headless internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) + .Bind().ToConstant(new ManagedDispatcherImpl(null)) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs deleted file mode 100644 index 046e4645e3..0000000000 --- a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs +++ /dev/null @@ -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 Signaled; - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 9dc7f08064..af4a70f128 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/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) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index 686050e7c2..61c94a48bd 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -14,12 +14,10 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private int _epoll; private Action _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 onInput) { _onInput = onInput; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index bff9ddc55c..300cbc2689 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/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 _onInput; private Dictionary _pointers = new Dictionary(); 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) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index c3e90f5fd7..bc178c8ecb 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/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().ToConstant(gl.PlatformGraphics); var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(Threading) + .Bind().ToConstant(new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))) .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index 66b24e9722..cdcd5af142 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/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; @@ -34,6 +35,19 @@ class ManualRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue } } +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 handler)> _inputQueue = new(); diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 81474d5efb..fd258dfd96 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -17,6 +17,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Threading; +using Avalonia.Utilities; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; #if AVALONIA_SKIA @@ -122,7 +123,8 @@ namespace Avalonia.Direct2D1.RenderTests // Free pools for (var c = 0; c < 11; c++) - TestThreadingInterface.RunTimers(); + foreach (var dp in Dispatcher.SnapshotTimersForUnitTests()) + dp.ForceFire(); writableBitmap.Save(compositedPath); } } From 1cdb876847ab50cfdae9fe4aaf565869632fd3c7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 19:51:05 +0600 Subject: [PATCH 09/58] Revert SynchronizationContext::Post --- src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index b8ac83f418..1b29cf32f7 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -30,7 +30,7 @@ namespace Avalonia.Threading /// public override void Post(SendOrPostCallback d, object? state) { - Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Background); + Dispatcher.UIThread.Post(d, state, DispatcherPriority.Background); } /// From 1d6e974e9cc8b8d85b037c7158bc58621613d523 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Mar 2023 15:38:20 +0100 Subject: [PATCH 10/58] Added failing test for #10655. --- .../AvaloniaObjectTests_SetCurrentValue.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index c850fbdb08..d2299056d6 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -351,6 +351,47 @@ 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().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); + } + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) { return target.GetDiagnostic(property).Priority; From 5a117d37c0d1941e0dd1b64ad571d7e7fd55fdfc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Mar 2023 16:00:07 +0100 Subject: [PATCH 11/58] Fix current values being overridden. At the beginning of a re-evaluation pass we mark all effective values as unset in order to signal that we've not yet found a value for them in the value frames. We do this in all cases except when they have a local value: in the case of a local value, the effective value _is_ the value and so value frames should only take effect when they're higher priority than the effective value. However when I added `SetCurrentValue` support, I neglected to change this logic to also take into account current values. Fixes #10655 Fixes #10671 --- src/Avalonia.Base/PropertyStore/EffectiveValue.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 11a4dd7893..7e9f9ae9ba 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -54,9 +54,9 @@ namespace Avalonia.PropertyStore /// public void BeginReevaluation(bool clearLocalValue = false) { - if (clearLocalValue || Priority != BindingPriority.LocalValue) + if (clearLocalValue || (Priority != BindingPriority.LocalValue && !IsOverridenCurrentValue)) Priority = BindingPriority.Unset; - if (clearLocalValue || BasePriority != BindingPriority.LocalValue) + if (clearLocalValue || (BasePriority != BindingPriority.LocalValue && !IsOverridenCurrentValue)) BasePriority = BindingPriority.Unset; } From ccc0efaacde4740fcf86c7240565e4ed5ad4cecd Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 21:36:12 +0600 Subject: [PATCH 12/58] Fixed DispatcherTimerHelper for unit tests --- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 5 ++++- src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 0ce2479a45..eedfe12734 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -16,7 +16,10 @@ public partial class Dispatcher { _backgroundTimer = new DispatcherTimer(this, DispatcherPriority.Send, - TimeSpan.FromMilliseconds(1)); + TimeSpan.FromMilliseconds(1)) + { + Tag = "Dispatcher.RequestBackgroundProcessing" + }; _backgroundTimer.Tick += delegate { _backgroundTimer.Stop(); diff --git a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs b/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs index 0c4a5f1051..a457388fb2 100644 --- a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs +++ b/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs @@ -12,6 +12,7 @@ public static class DispatcherTimerUtils public static void ForceFire(this DispatcherTimer timer) { timer.Promote(); + timer.Dispatcher.RemoveTimer(timer); timer.Dispatcher.RunJobs(); } } \ No newline at end of file From 763c53c2c78ced844f18cbf433208697b7372cb1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 17 Mar 2023 22:07:32 +0300 Subject: [PATCH 13/58] Allow sync wait for non-UI threads --- src/Avalonia.Base/Threading/DispatcherOperation.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index b0d7e5a049..173ab81ef8 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -253,10 +253,9 @@ public class DispatcherOperation : DispatcherOperation { get { - if (TaskCompletionSource.Task.IsCompleted) + if (TaskCompletionSource.Task.IsCompleted || !Dispatcher.CheckAccess()) return TaskCompletionSource.Task.GetAwaiter().GetResult(); throw new InvalidOperationException("Synchronous wait is only supported on non-UI threads"); - } } } @@ -306,4 +305,4 @@ public enum DispatcherOperationStatus Aborted = 1, Completed = 2, Executing = 3, -} \ No newline at end of file +} From ea5623df7e645936ff94fdd29543c259fb8e1142 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 15:27:26 -0400 Subject: [PATCH 14/58] Fix initial state/overlay & make TemplateSettings a readonly DirectProperty --- src/Avalonia.Controls/SplitView/SplitView.cs | 124 ++++++++++++------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 1099a40f08..a5c87f5843 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -94,8 +94,9 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly StyledProperty TemplateSettingsProperty = - AvaloniaProperty.Register(nameof(TemplateSettings)); + public static readonly DirectProperty TemplateSettingsProperty = + AvaloniaProperty.RegisterDirect(nameof(TemplateSettings), + x => x.TemplateSettings); /// /// Defines the event. @@ -131,12 +132,12 @@ namespace Avalonia.Controls private Panel? _pane; private IDisposable? _pointerDisposable; + private SplitViewTemplateSettings _templateSettings; + private string _lastDisplayModePseudoclass; + private string _lastPlacementPseudoclass; public SplitView() { - PseudoClasses.Add(":overlay"); - PseudoClasses.Add(":left"); - TemplateSettings = new SplitViewTemplateSettings(); } @@ -208,7 +209,7 @@ namespace Avalonia.Controls get => GetValue(PaneProperty); set => SetValue(PaneProperty, value); } - + /// /// Gets or sets the data template used to display the header content of the control. /// @@ -235,8 +236,8 @@ namespace Avalonia.Controls /// public SplitViewTemplateSettings TemplateSettings { - get => GetValue(TemplateSettingsProperty); - set => SetValue(TemplateSettingsProperty, value); + get => _templateSettings; + private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value); } /// @@ -291,7 +292,7 @@ namespace Avalonia.Controls { return true; } - + return result; } @@ -299,12 +300,20 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); _pane = e.NameScope.Find("PART_PaneRoot"); + + UpdateVisualStateForCompactPaneLength(CompactPaneLength); + UpdateVisualStateForDisplayMode(DisplayMode); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); + // :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); + var topLevel = this.VisualRoot; if (topLevel is Window window) { @@ -325,36 +334,11 @@ namespace Avalonia.Controls if (change.Property == CompactPaneLengthProperty) { - var newLen = change.GetNewValue(); - 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()); } else if (change.Property == DisplayModeProperty) { - var oldState = GetPseudoClass(change.GetOldValue()); - var newState = GetPseudoClass(change.GetNewValue()); - - PseudoClasses.Remove($":{oldState}"); - PseudoClasses.Add($":{newState}"); - - var (closedPaneWidth, paneColumnGridLength) = change.GetNewValue() 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()); } else if (change.Property == IsPaneOpenProperty) { @@ -389,10 +373,7 @@ namespace Avalonia.Controls } else if (change.Property == PanePlacementProperty) { - var oldState = GetPseudoClass(change.GetOldValue()); - var newState = GetPseudoClass(change.GetNewValue()); - PseudoClasses.Remove($":{oldState}"); - PseudoClasses.Add($":{newState}"); + UpdateVisualStateForPanePlacementProperty(change.GetNewValue()); } else if (change.Property == UseLightDismissOverlayModeProperty) { @@ -438,7 +419,7 @@ namespace Avalonia.Controls e.Handled = true; } } - + private bool ShouldClosePane() { return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); @@ -471,14 +452,14 @@ namespace Avalonia.Controls { return mode switch { - SplitViewDisplayMode.Inline => "inline", - SplitViewDisplayMode.CompactInline => "compactinline", - SplitViewDisplayMode.Overlay => "overlay", - SplitViewDisplayMode.CompactOverlay => "compactoverlay", + SplitViewDisplayMode.Inline => ":inline", + SplitViewDisplayMode.CompactInline => ":compactinline", + SplitViewDisplayMode.Overlay => ":overlay", + SplitViewDisplayMode.CompactOverlay => ":compactoverlay", _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) }; } - + /// /// Gets the appropriate PseudoClass for the given . /// @@ -486,8 +467,8 @@ namespace Avalonia.Controls { return placement switch { - SplitViewPanePlacement.Left => "left", - SplitViewPanePlacement.Right => "right", + SplitViewPanePlacement.Left => ":left", + SplitViewPanePlacement.Right => ":right", _ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null) }; } @@ -519,6 +500,53 @@ 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); + } + /// /// Coerces/validates the property value. /// From 878e18509e36f53f09256b1796b2f1cad6f02b52 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 15:33:03 -0400 Subject: [PATCH 15/58] Use const strings for pseudoclasses --- src/Avalonia.Controls/SplitView/SplitView.cs | 45 +++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index a5c87f5843..786a26ae68 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -15,18 +15,21 @@ namespace Avalonia.Controls /// A control with two views: A collapsible pane and an area for content /// [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"; /// /// Defines the property @@ -346,15 +349,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)); } @@ -378,7 +381,7 @@ namespace Avalonia.Controls else if (change.Property == UseLightDismissOverlayModeProperty) { var mode = change.GetNewValue(); - PseudoClasses.Set(":lightdismiss", mode); + PseudoClasses.Set(pcLightDismiss, mode); } } @@ -452,10 +455,10 @@ 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) }; } @@ -467,8 +470,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) }; } From 25825e4eabc8e7c1525b89511de94ab0eba5e561 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 16:57:55 -0400 Subject: [PATCH 16/58] Fix typo in pseudoclass --- src/Avalonia.Controls/SplitView/SplitView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 786a26ae68..9fd316a9f7 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls protected const string pcOpen = ":open"; protected const string pcClosed = ":closed"; protected const string pcCompactOverlay = ":compactoverlay"; - protected const string pcCompactInline = ":compactInline"; + protected const string pcCompactInline = ":compactinline"; protected const string pcOverlay = ":overlay"; protected const string pcInline = ":inline"; protected const string pcLeft = ":left"; From 87a2bb6bda76d5370bae07fd62951971c4d2f70e Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 16:58:20 -0400 Subject: [PATCH 17/58] Improve light dismiss behavior --- src/Avalonia.Controls/SplitView/SplitView.cs | 70 +++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 9fd316a9f7..b8a697e309 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/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 { @@ -144,10 +145,6 @@ namespace Avalonia.Controls TemplateSettings = new SplitViewTemplateSettings(); } - static SplitView() - { - } - /// /// Gets or sets the length of the pane when in /// or mode @@ -316,12 +313,6 @@ namespace Avalonia.Controls // soon as we're attached so the template applies. The other visual states can // be updated after the template applies UpdateVisualStateForPanePlacementProperty(PanePlacement); - - var topLevel = this.VisualRoot; - if (topLevel is Window window) - { - _pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); - } } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -385,21 +376,27 @@ namespace Avalonia.Controls } } - 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); + } } - //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) { @@ -416,6 +413,7 @@ namespace Avalonia.Controls src = src.VisualParent; } + if (closePane) { SetCurrentValue(IsPaneOpenProperty, false); @@ -423,7 +421,7 @@ namespace Avalonia.Controls } } - private bool ShouldClosePane() + private bool IsInOverlayMode() { return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); } @@ -435,6 +433,7 @@ namespace Avalonia.Controls protected virtual void OnPaneOpened(RoutedEventArgs args) { + EnableLightDismiss(); RaiseEvent(args); } @@ -445,6 +444,8 @@ namespace Avalonia.Controls protected virtual void OnPaneClosed(RoutedEventArgs args) { + _pointerDisposable?.Dispose(); + _pointerDisposable = null; RaiseEvent(args); } @@ -550,6 +551,39 @@ namespace Avalonia.Controls 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; + } + /// /// Coerces/validates the property value. /// From ffdab9436563b24c4705b659571ebb45047f5015 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:27:17 -0400 Subject: [PATCH 18/58] Only need to update display mode with ApplyTemplate --- src/Avalonia.Controls/SplitView/SplitView.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index b8a697e309..72e8140d09 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -301,7 +301,6 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); _pane = e.NameScope.Find("PART_PaneRoot"); - UpdateVisualStateForCompactPaneLength(CompactPaneLength); UpdateVisualStateForDisplayMode(DisplayMode); } From a3c8210f2ee7c0041ebfe678f1609958390766e6 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:27:29 -0400 Subject: [PATCH 19/58] Add new tests --- .../SplitViewTests.cs | 224 +++++++++++++++++- 1 file changed, 221 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs index 03653ec42c..9b43c469ba 100644 --- a/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Text; +using Avalonia.Input; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -62,5 +61,224 @@ namespace Avalonia.Controls.UnitTests Assert.True(splitView.IsPaneOpen); } + + [Fact] + public void SplitView_TemplateSettings_Are_Correct_For_Display_Modes() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + var wnd = new Window + { + Width = 1280, + Height = 720 + }; + var splitView = new SplitView(); + wnd.Content = splitView; + wnd.Show(); + + var zeroGridLength = new GridLength(0); + var compactLength = splitView.CompactPaneLength; + var compactGridLength = new GridLength(compactLength); + + // Overlay is default DisplayMode + Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth); + Assert.Equal(zeroGridLength, splitView.TemplateSettings.PaneColumnGridLength); + + splitView.DisplayMode = SplitViewDisplayMode.CompactOverlay; + Assert.Equal(compactLength, splitView.TemplateSettings.ClosedPaneWidth); + Assert.Equal(compactGridLength, splitView.TemplateSettings.PaneColumnGridLength); + + splitView.DisplayMode = SplitViewDisplayMode.Inline; + Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth); + Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength); + + splitView.DisplayMode = SplitViewDisplayMode.CompactInline; + Assert.Equal(compactLength, splitView.TemplateSettings.ClosedPaneWidth); + Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength); + } + + [Fact] + public void SplitView_TemplateSettings_Update_With_CompactPaneLength() + { + var splitView = new SplitView(); + + // CompactInline: + // - ClosedPaneWidth = CompactPaneLength + // - PaneColumnGridLength = Auto + splitView.DisplayMode = SplitViewDisplayMode.CompactInline; + + var compactLength = splitView.CompactPaneLength; + + Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength); + Assert.Equal(compactLength, splitView.TemplateSettings.ClosedPaneWidth); + + splitView.CompactPaneLength = 100; + + Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength); + Assert.Equal(100, splitView.TemplateSettings.ClosedPaneWidth); + + // CompactOverlay: + // - ClosedPaneWidth = CompactPaneLength + // - PaneColumnGridLength = GridLength { CompactPaneLength, Pixel } + splitView.DisplayMode = SplitViewDisplayMode.CompactOverlay; + splitView.CompactPaneLength = 50; + + Assert.Equal(new GridLength(50), splitView.TemplateSettings.PaneColumnGridLength); + Assert.Equal(50, splitView.TemplateSettings.ClosedPaneWidth); + + // Value shouldn't change for these - changing the display mode will update + // the template settings with the right value + splitView.DisplayMode = SplitViewDisplayMode.Inline; + splitView.CompactPaneLength = 1; + + Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength); + Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth); + + splitView.DisplayMode = SplitViewDisplayMode.Overlay; + splitView.CompactPaneLength = 2; + + Assert.Equal(new GridLength(0), splitView.TemplateSettings.PaneColumnGridLength); + Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth); + } + + [Fact] + public void SplitView_Pointer_Closes_Pane_In_Overlay_Mode() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow + .With(globalClock: new MockGlobalClock())); + var wnd = new Window + { + Width = 1280, + Height = 720 + }; + var splitView = new SplitView(); + wnd.Content = splitView; + wnd.Show(); + + splitView.IsPaneOpen = true; + + splitView.RaiseEvent(new PointerReleasedEventArgs(splitView, + null, wnd, new Point(1270, 30), 0, + new PointerPointProperties(), + KeyModifiers.None, + MouseButton.Left)); + + Assert.False(splitView.IsPaneOpen); + + // Inline shouldn't close the pane + splitView.DisplayMode = SplitViewDisplayMode.Inline; + splitView.IsPaneOpen = true; + + splitView.RaiseEvent(new PointerReleasedEventArgs(splitView, + null, wnd, new Point(1270, 30), 0, + new PointerPointProperties(), + KeyModifiers.None, + MouseButton.Left)); + + Assert.True(splitView.IsPaneOpen); + } + + [Fact] + public void SplitView_Pointer_Should_Not_Close_Pane_If_Over_Pane() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow + .With(globalClock: new MockGlobalClock())); + var wnd = new Window + { + Width = 1280, + Height = 720 + }; + var clickBorder = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = Layout.HorizontalAlignment.Left, + VerticalAlignment = Layout.VerticalAlignment.Top + }; + var splitView = new SplitView + { + Pane = clickBorder + }; + wnd.Content = splitView; + wnd.Show(); + + splitView.IsPaneOpen = true; + + clickBorder.RaiseEvent(new PointerReleasedEventArgs(splitView, + null, wnd, new Point(5, 5), 0, + new PointerPointProperties(), + KeyModifiers.None, + MouseButton.Left)); + + Assert.True(splitView.IsPaneOpen); + } + + [Fact] + public void SplitView_Escape_Key_Closes_Light_Dismissable_Pane() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow + .With(globalClock: new MockGlobalClock())); + var wnd = new Window + { + Width = 1280, + Height = 720 + }; + var button = new Button(); + var splitView = new SplitView + { + Pane = button + }; + wnd.Content = splitView; + wnd.Show(); + + splitView.IsPaneOpen = true; + + button.RaiseEvent(new KeyEventArgs + { + Key = Key.Escape, + RoutedEvent = InputElement.KeyDownEvent + }); + + Assert.False(splitView.IsPaneOpen); + + splitView.DisplayMode = SplitViewDisplayMode.Inline; + + splitView.IsPaneOpen = true; + + button.RaiseEvent(new KeyEventArgs + { + Key = Key.Escape, + RoutedEvent = InputElement.KeyDownEvent + }); + + Assert.True(splitView.IsPaneOpen); + } + + [Fact] + public void Top_Level_Back_Requested_Closes_Light_Dismissable_Pane() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow + .With(globalClock: new MockGlobalClock())); + var wnd = new Window + { + Width = 1280, + Height = 720 + }; + var splitView = new SplitView(); + wnd.Content = splitView; + wnd.Show(); + + splitView.IsPaneOpen = true; + + wnd.RaiseEvent(new Interactivity.RoutedEventArgs(TopLevel.BackRequestedEvent)); + + Assert.False(splitView.IsPaneOpen); + + splitView.DisplayMode = SplitViewDisplayMode.Inline; + splitView.IsPaneOpen = true; + + wnd.RaiseEvent(new Interactivity.RoutedEventArgs(TopLevel.BackRequestedEvent)); + + Assert.True(splitView.IsPaneOpen); + } } } From acc8b9fe52a734af6810a2f411b1ab6aba391123 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:28:04 -0400 Subject: [PATCH 20/58] Handle KeyDown if pane close is triggered --- src/Avalonia.Controls/SplitView/SplitView.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 72e8140d09..8501d518a7 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -382,6 +382,7 @@ namespace Avalonia.Controls if (IsPaneOpen && IsInOverlayMode()) { SetCurrentValue(IsPaneOpenProperty, false); + e.Handled = true; } } From b5be1ef9b0494c8e357037c39f20fee1e2a39fa6 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:38:57 -0400 Subject: [PATCH 21/58] Fix nullable stuff --- src/Avalonia.Controls/SplitView/SplitView.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 8501d518a7..93b0ffe57f 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -136,9 +136,9 @@ namespace Avalonia.Controls private Panel? _pane; private IDisposable? _pointerDisposable; - private SplitViewTemplateSettings _templateSettings; - private string _lastDisplayModePseudoclass; - private string _lastPlacementPseudoclass; + private SplitViewTemplateSettings _templateSettings = null!; + private string? _lastDisplayModePseudoclass; + private string? _lastPlacementPseudoclass; public SplitView() { @@ -575,7 +575,7 @@ namespace Avalonia.Controls } } - private void TopLevelBackRequested(object sender, RoutedEventArgs e) + private void TopLevelBackRequested(object? sender, RoutedEventArgs e) { if (!IsInOverlayMode()) return; From ed8a4fa7d5b13e751b5c7f7c1f2c477ddba95420 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Fri, 17 Mar 2023 21:29:45 -0400 Subject: [PATCH 22/58] Just init TemplateSettings --- src/Avalonia.Controls/SplitView/SplitView.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 93b0ffe57f..8060ca9594 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -136,15 +136,10 @@ namespace Avalonia.Controls private Panel? _pane; private IDisposable? _pointerDisposable; - private SplitViewTemplateSettings _templateSettings = null!; + private SplitViewTemplateSettings _templateSettings = new SplitViewTemplateSettings(); private string? _lastDisplayModePseudoclass; private string? _lastPlacementPseudoclass; - public SplitView() - { - TemplateSettings = new SplitViewTemplateSettings(); - } - /// /// Gets or sets the length of the pane when in /// or mode From 067df73f9708d5078038647d2273a1e190e44db5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 16:26:37 +0600 Subject: [PATCH 23/58] Naming --- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 2 +- src/Avalonia.Base/Threading/Dispatcher.Timers.cs | 16 ++++++++-------- src/Avalonia.Base/Threading/IDispatcherImpl.cs | 10 +++++----- .../Platform/ManagedDispatcherImpl.cs | 6 +++--- src/Avalonia.Native/DispatcherImpl.cs | 4 ++-- src/Avalonia.X11/X11PlatformThreading.cs | 4 ++-- .../Avalonia.Win32/Win32DispatcherImpl.cs | 6 +++--- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index eedfe12734..1550da079f 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -59,7 +59,7 @@ public partial class Dispatcher public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index e71e7375d3..0f9924d90d 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -9,7 +9,7 @@ public partial class Dispatcher private List _timers = new(); private long _timersVersion; private bool _dueTimeFound; - private int _dueTimeInTicks; + private int _dueTimeInMs; private bool _isOsTimerSet; internal void UpdateOSTimer() @@ -25,9 +25,9 @@ public partial class Dispatcher if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read { bool oldDueTimeFound = _dueTimeFound; - int oldDueTimeInTicks = _dueTimeInTicks; + int oldDueTimeInTicks = _dueTimeInMs; _dueTimeFound = false; - _dueTimeInTicks = 0; + _dueTimeInMs = 0; if (_timers.Count > 0) { @@ -36,19 +36,19 @@ public partial class Dispatcher { var timer = _timers[i]; - if (!_dueTimeFound || timer.DueTimeInMs - _dueTimeInTicks < 0) + if (!_dueTimeFound || timer.DueTimeInMs - _dueTimeInMs < 0) { _dueTimeFound = true; - _dueTimeInTicks = timer.DueTimeInMs; + _dueTimeInMs = timer.DueTimeInMs; } } } if (_dueTimeFound) { - if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInTicks)) + if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs)) { - _impl.UpdateTimer(Math.Max(1, _dueTimeInTicks)); + _impl.UpdateTimer(Math.Max(1, _dueTimeInMs)); _isOsTimerSet = true; } } @@ -111,7 +111,7 @@ public partial class Dispatcher { if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read { - if (_dueTimeFound && _dueTimeInTicks - currentTimeInTicks <= 0) + if (_dueTimeFound && _dueTimeInMs - currentTimeInTicks <= 0) { timers = _timers; timersVersion = _timersVersion; diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 089f5cb660..ab501f698c 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -12,7 +12,7 @@ public interface IDispatcherImpl void Signal(); event Action Signaled; event Action Timer; - void UpdateTimer(int? dueTimeInTicks); + void UpdateTimer(int? dueTimeInMs); } @@ -46,13 +46,13 @@ internal class LegacyDispatcherImpl : IControlledDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { _timer?.Dispose(); _timer = null; - if (dueTimeInTicks.HasValue) + if (dueTimeInMs.HasValue) _timer = _platformThreading.StartTimer(DispatcherPriority.Send, - TimeSpan.FromMilliseconds(dueTimeInTicks.Value), + TimeSpan.FromMilliseconds(dueTimeInMs.Value), OnTick); } @@ -80,7 +80,7 @@ class NullDispatcherImpl : IDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { } diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index bc7595ef4e..7a487e99fb 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -40,13 +40,13 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { lock (_lock) { - _nextTimer = dueTimeInTicks == null + _nextTimer = dueTimeInMs == null ? null - : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInTicks.Value); + : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInMs.Value); if (!CurrentThreadIsLoopThread) _wakeup.Set(); } diff --git a/src/Avalonia.Native/DispatcherImpl.cs b/src/Avalonia.Native/DispatcherImpl.cs index 4788dd5e82..a9e5e6deb1 100644 --- a/src/Avalonia.Native/DispatcherImpl.cs +++ b/src/Avalonia.Native/DispatcherImpl.cs @@ -54,9 +54,9 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public void Signal() => _native.Signal(); - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { - var ms = dueTimeInTicks == null ? -1 : Math.Max(1, dueTimeInTicks.Value - TickCount); + var ms = dueTimeInMs == null ? -1 : Math.Max(1, dueTimeInMs.Value - TickCount); _native.UpdateTimer(ms); } diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/X11PlatformThreading.cs index b8a5b68658..f2f45bce8e 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/X11PlatformThreading.cs @@ -227,9 +227,9 @@ namespace Avalonia.X11 public event Action Signaled; public event Action Timer; - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { - _nextTimer = dueTimeInTicks; + _nextTimer = dueTimeInMs; if (_nextTimer != null) Wakeup(); } diff --git a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs index 0916490ed8..72a60902ba 100644 --- a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs +++ b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs @@ -39,14 +39,14 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock void TimerProc(IntPtr hWnd, uint uMsg, IntPtr nIdEvent, uint dwTime) => Timer?.Invoke(); - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(int? dueTimeInMs) { if (_timerHandle.HasValue) KillTimer(IntPtr.Zero, _timerHandle.Value); - if (dueTimeInTicks == null) + if (dueTimeInMs == null) return; - var interval = (uint)Math.Max(1, TickCount - dueTimeInTicks.Value); + var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); _timerHandle = SetTimer( IntPtr.Zero, From 718089bda93d2cf2e0bf063dfdab1968dc0c4d25 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 16:35:09 +0600 Subject: [PATCH 24/58] Fixed some timers --- src/Avalonia.Base/Threading/IDispatcherImpl.cs | 8 ++++++-- src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index ab501f698c..7f1e352b51 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -30,7 +30,7 @@ public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput void RunLoop(CancellationToken token); } -internal class LegacyDispatcherImpl : IControlledDispatcherImpl +internal class LegacyDispatcherImpl : DefaultDispatcherClock, IControlledDispatcherImpl { private readonly IPlatformThreadingInterface _platformThreading; private IDisposable? _timer; @@ -50,10 +50,14 @@ internal class LegacyDispatcherImpl : IControlledDispatcherImpl { _timer?.Dispose(); _timer = null; + if (dueTimeInMs.HasValue) + { + var interval = Math.Max(1, dueTimeInMs.Value - TickCount); _timer = _platformThreading.StartTimer(DispatcherPriority.Send, - TimeSpan.FromMilliseconds(dueTimeInMs.Value), + TimeSpan.FromMilliseconds(interval), OnTick); + } } private void OnTick() diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index 7a487e99fb..54c96113ea 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -46,7 +46,7 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl { _nextTimer = dueTimeInMs == null ? null - : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInMs.Value); + : TimeSpan.FromMilliseconds(dueTimeInMs.Value); if (!CurrentThreadIsLoopThread) _wakeup.Set(); } From d96b8124ed63c72a9ca176fb17d4a1dc8b09e808 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 17:03:04 +0600 Subject: [PATCH 25/58] Don't use DispatcherTimer infrastructure for background processing --- .../Threading/Dispatcher.Queue.cs | 31 +++++----- .../Threading/Dispatcher.Timers.cs | 59 ++++++++++++++----- .../Threading/DispatcherTimer.cs | 2 +- .../DispatcherTests.cs | 25 +++----- 4 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 1550da079f..4360d8111d 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -7,26 +7,18 @@ public partial class Dispatcher { private readonly DispatcherPriorityQueue _queue = new(); private bool _signaled; - private DispatcherTimer? _backgroundTimer; private const int MaximumTimeProcessingBackgroundJobs = 50; void RequestBackgroundProcessing() { - if (_backgroundTimer == null) + lock (InstanceLock) { - _backgroundTimer = - new DispatcherTimer(this, DispatcherPriority.Send, - TimeSpan.FromMilliseconds(1)) - { - Tag = "Dispatcher.RequestBackgroundProcessing" - }; - _backgroundTimer.Tick += delegate + if (_dueTimeForBackgroundProcessing == null) { - _backgroundTimer.Stop(); - }; + _dueTimeForBackgroundProcessing = Clock.TickCount + 1; + UpdateOSTimer(); + } } - - _backgroundTimer.IsEnabled = true; } /// @@ -167,8 +159,19 @@ public partial class Dispatcher { lock (InstanceLock) { + if (!CheckAccess()) + { + RequestForegroundProcessing(); + return true; + } + if (_queue.MaxPriority <= DispatcherPriority.Input) - RequestBackgroundProcessing(); + { + if (_pendingInputImpl is { CanQueryPendingInput: true, HasPendingInput: false }) + RequestForegroundProcessing(); + else + RequestBackgroundProcessing(); + } else RequestForegroundProcessing(); } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index 0f9924d90d..0bf087918e 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -10,13 +10,30 @@ public partial class Dispatcher private long _timersVersion; private bool _dueTimeFound; private int _dueTimeInMs; - private bool _isOsTimerSet; - internal void UpdateOSTimer() + private int? _dueTimeForTimers; + private int? _dueTimeForBackgroundProcessing; + private int? _osTimerSetTo; + + private void UpdateOSTimer() + { + lock (InstanceLock) + { + var nextDueTime = + (_dueTimeForTimers.HasValue && _dueTimeForBackgroundProcessing.HasValue) + ? Math.Min(_dueTimeForTimers.Value, _dueTimeForBackgroundProcessing.Value) + : _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; + if(_osTimerSetTo == nextDueTime) + return; + _impl.UpdateTimer(_osTimerSetTo = nextDueTime); + } + } + + internal void UpdateOSTimerForTimers() { if (!CheckAccess()) { - Post(UpdateOSTimer, DispatcherPriority.Send); + Post(UpdateOSTimerForTimers, DispatcherPriority.Send); return; } @@ -46,16 +63,16 @@ public partial class Dispatcher if (_dueTimeFound) { - if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs)) + if (_dueTimeForTimers == null || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs)) { - _impl.UpdateTimer(Math.Max(1, _dueTimeInMs)); - _isOsTimerSet = true; + _dueTimeForTimers = _dueTimeInMs; + UpdateOSTimer(); } } else if (oldDueTimeFound) { - _impl.UpdateTimer(null); - _isOsTimerSet = false; + _dueTimeForTimers = null; + UpdateOSTimer(); } } } @@ -72,7 +89,7 @@ public partial class Dispatcher } } - UpdateOSTimer(); + UpdateOSTimerForTimers(); } internal void RemoveTimer(DispatcherTimer timer) @@ -86,17 +103,29 @@ public partial class Dispatcher } } - UpdateOSTimer(); + UpdateOSTimerForTimers(); } private void OnOSTimer() { + bool needToPromoteTimers = false; + bool needToProcessQueue = false; lock (InstanceLock) { - _impl.UpdateTimer(null); - _isOsTimerSet = false; + needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Clock.TickCount; + if (needToPromoteTimers) + _dueTimeForTimers = null; + needToProcessQueue = _dueTimeForBackgroundProcessing.HasValue && + _dueTimeForBackgroundProcessing.Value <= Clock.TickCount; + if (needToProcessQueue) + _dueTimeForBackgroundProcessing = null; + UpdateOSTimer(); } - PromoteTimers(); + + if (needToPromoteTimers) + PromoteTimers(); + if (needToProcessQueue) + ExecuteJobsCore(); } internal void PromoteTimers() @@ -166,10 +195,10 @@ public partial class Dispatcher } finally { - UpdateOSTimer(); + UpdateOSTimerForTimers(); } } internal static List SnapshotTimersForUnitTests() => - s_uiThread!._timers.Where(t => t != s_uiThread._backgroundTimer).ToList(); + s_uiThread!._timers.ToList(); } \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 183f66eb61..f7ed03379e 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -132,7 +132,7 @@ public partial class DispatcherTimer if (updateOSTimer) { - _dispatcher.UpdateOSTimer(); + _dispatcher.UpdateOSTimerForTimers(); } } } diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index 450c857cfe..902af94121 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -99,15 +99,12 @@ public class DispatcherTests Assert.False(impl.AskedForSignal); Assert.NotNull(impl.NextTimer); - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); - Assert.Null(impl.NextTimer); - for (var c = 0; c < 4; c++) { - if (impl.NextTimer != null) - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); + Assert.NotNull(impl.NextTimer); + Assert.False(impl.AskedForSignal); + impl.ExecuteTimer(); + Assert.False(impl.AskedForSignal); impl.ExecuteSignal(); var expectedCount = (c + 1) * 3; if (c == 3) @@ -129,7 +126,7 @@ public class DispatcherTests public void DispatcherStopsItemProcessingWhenInputIsPending() { var impl = new SimpleDispatcherImpl(); - impl.TestInputPending = false; + impl.TestInputPending = true; var disp = new Dispatcher(impl, impl); var actions = new List(); for (var c = 0; c < 10; c++) @@ -144,17 +141,13 @@ public class DispatcherTests } Assert.False(impl.AskedForSignal); Assert.NotNull(impl.NextTimer); - - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); - Assert.Null(impl.NextTimer); + impl.TestInputPending = false; for (var c = 0; c < 4; c++) { - if (impl.NextTimer != null) - impl.ExecuteTimer(); - Assert.True(impl.AskedForSignal); - impl.ExecuteSignal(); + Assert.NotNull(impl.NextTimer); + impl.ExecuteTimer(); + Assert.False(impl.AskedForSignal); var expectedCount = c switch { 0 => 1, From fe4945d395f4a119a340decf8fc311f98c7bcfd2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Mar 2023 18:55:00 +0300 Subject: [PATCH 26/58] Win32 dispatcher fixes --- .../Threading/Dispatcher.Timers.cs | 35 ++++++++++--------- .../Threading/DispatcherTimer.cs | 2 +- .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/Win32DispatcherImpl.cs | 30 ++++++++-------- src/Windows/Avalonia.Win32/Win32Platform.cs | 29 +++++---------- 5 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index 0bf087918e..269d10707e 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -17,23 +17,21 @@ public partial class Dispatcher private void UpdateOSTimer() { - lock (InstanceLock) - { - var nextDueTime = - (_dueTimeForTimers.HasValue && _dueTimeForBackgroundProcessing.HasValue) - ? Math.Min(_dueTimeForTimers.Value, _dueTimeForBackgroundProcessing.Value) - : _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; - if(_osTimerSetTo == nextDueTime) - return; - _impl.UpdateTimer(_osTimerSetTo = nextDueTime); - } + 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 UpdateOSTimerForTimers() + internal void RescheduleTimers() { if (!CheckAccess()) { - Post(UpdateOSTimerForTimers, DispatcherPriority.Send); + Post(RescheduleTimers, DispatcherPriority.Send); return; } @@ -89,7 +87,7 @@ public partial class Dispatcher } } - UpdateOSTimerForTimers(); + RescheduleTimers(); } internal void RemoveTimer(DispatcherTimer timer) @@ -103,15 +101,18 @@ public partial class Dispatcher } } - UpdateOSTimerForTimers(); + 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 <= Clock.TickCount; if (needToPromoteTimers) _dueTimeForTimers = null; @@ -119,13 +120,13 @@ public partial class Dispatcher _dueTimeForBackgroundProcessing.Value <= Clock.TickCount; if (needToProcessQueue) _dueTimeForBackgroundProcessing = null; - UpdateOSTimer(); } if (needToPromoteTimers) PromoteTimers(); if (needToProcessQueue) ExecuteJobsCore(); + UpdateOSTimer(); } internal void PromoteTimers() @@ -195,10 +196,10 @@ public partial class Dispatcher } finally { - UpdateOSTimerForTimers(); + RescheduleTimers(); } } internal static List SnapshotTimersForUnitTests() => s_uiThread!._timers.ToList(); -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index f7ed03379e..0c235ee161 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -132,7 +132,7 @@ public partial class DispatcherTimer if (updateOSTimer) { - _dispatcher.UpdateOSTimerForTimers(); + _dispatcher.RescheduleTimers(); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 9618a5b4cd..7510b48270 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1796,7 +1796,7 @@ namespace Avalonia.Win32.Interop QS_SENDMESSAGE = 0x0040, QS_HOTKEY = 0x0080, QS_ALLPOSTMESSAGE = 0x0100, - QS_EVENT = 0x0200, + 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, diff --git a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs index 72a60902ba..3c2f7842ba 100644 --- a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs +++ b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs @@ -10,13 +10,10 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock { private readonly IntPtr _messageWindow; private static Thread? s_uiThread; - private IntPtr? _timerHandle; - private readonly TimerProc _timerDelegate; public Win32DispatcherImpl(IntPtr messageWindow) { _messageWindow = messageWindow; s_uiThread = Thread.CurrentThread; - _timerDelegate = TimerProc; } public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread; @@ -37,26 +34,27 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public event Action? Signaled; public event Action? Timer; - void TimerProc(IntPtr hWnd, uint uMsg, IntPtr nIdEvent, uint dwTime) => Timer?.Invoke(); + public void FireTimer() => Timer?.Invoke(); public void UpdateTimer(int? dueTimeInMs) { - if (_timerHandle.HasValue) - KillTimer(IntPtr.Zero, _timerHandle.Value); if (dueTimeInMs == null) - return; - - var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); - - _timerHandle = SetTimer( - IntPtr.Zero, - IntPtr.Zero, - interval, - _timerDelegate); + { + KillTimer(_messageWindow, (IntPtr)Win32Platform.TIMERID_DISPATCHER); + } + else + { + var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); + SetTimer( + _messageWindow, + (IntPtr)Win32Platform.TIMERID_DISPATCHER, + interval, + null!); + } } public bool CanQueryPendingInput => true; - + public bool HasPendingInput { get diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 6a5841f36f..857af3a783 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -112,6 +112,7 @@ namespace Avalonia.Win32 private static readonly Win32Platform s_instance = new(); private static Win32PlatformOptions? s_options; private static Compositor? s_compositor; + internal const int TIMERID_DISPATCHER = 1; private WndProc? _wndProcDelegate; private IntPtr _hwnd; @@ -182,27 +183,7 @@ namespace Avalonia.Win32 s_compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), 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 event EventHandler? ShutdownRequested; [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] @@ -238,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); From 919697daa6098195873778605709bf5e02ac2c07 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 18 Mar 2023 19:51:19 +0100 Subject: [PATCH 27/58] Add additional failing test for SetCurrentValue. --- .../AvaloniaObjectTests_SetCurrentValue.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index d2299056d6..a7eb95ba40 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Diagnostics; @@ -392,6 +393,22 @@ namespace Avalonia.Base.UnitTests 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("initial"); + + target.Bind(Class1.FooProperty, source, priority); + target.SetCurrentValue(Class1.FooProperty, "current"); + source.OnNext("new"); + + Assert.Equal("new", target.Foo); + } + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) { return target.GetDiagnostic(property).Priority; From 101346326277e29293f8065afefd68fcf18f9bc1 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Mar 2023 15:07:06 -0400 Subject: [PATCH 28/58] Switch ColorHelper.ToDisplayName() to a hue-only algorithm This is not working quite as accurately as expected so will likely be replaced by a hybrid technique next. --- .../Helpers/ColorHelper.cs | 146 ++++++++++++++---- 1 file changed, 113 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index c1a03b1b77..0c47a8ed3f 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -1,6 +1,6 @@ using System; -using System.Globalization; using System.Collections.Generic; +using System.Globalization; using Avalonia.Media; using Avalonia.Utilities; @@ -11,8 +11,11 @@ namespace Avalonia.Controls.Primitives /// public static class ColorHelper { - private static readonly Dictionary cachedDisplayNames = new Dictionary(); - private static readonly object cacheMutex = new object(); + private static readonly Dictionary _cachedDisplayNames = new Dictionary(); + private static readonly Dictionary _cachedKnownColorHues = new Dictionary(); + private static readonly Dictionary _cachedKnownColorNames = new Dictionary(); + private static readonly object _displayNameCacheMutex = new object(); + private static readonly object _knownColorCacheMutex = new object(); /// /// Gets the relative (perceptual) luminance/brightness of the given color. @@ -74,37 +77,91 @@ namespace Avalonia.Controls.Primitives 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 hsvColor = color.ToHsv(); + + // Handle extremes that are outside the below algorithm + if (color.A == 0x00) + { + return GetDisplayName(KnownColor.Transparent); + } + else if (hsvColor.S <= 0.0) + { + return GetDisplayName(KnownColor.White); + } + else if (hsvColor.V <= 0.0) + { + return GetDisplayName(KnownColor.Black); + } // Attempt to use a previously cached display name - lock (cacheMutex) + lock (_displayNameCacheMutex) { - if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) + if (_cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) { return displayName; } } - // Find the closest known color by measuring 3D Euclidean distance (ignore alpha) + // Build KnownColor caches if they don't already exist + lock (_knownColorCacheMutex) + { + if (_cachedKnownColorHues.Count == 0 || + _cachedKnownColorNames.Count == 0) + { + _cachedKnownColorHues.Clear(); + _cachedKnownColorNames.Clear(); + + var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); + 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 (knownColor == KnownColor.Transparent) + { + continue; + } + + double hue = KnownColors.ToColor(knownColor).ToHsv().H; + + // 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 (!_cachedKnownColorHues.ContainsKey(knownColor)) + { + _cachedKnownColorHues.Add(knownColor, hue); + } + + if (!_cachedKnownColorNames.ContainsKey(knownColor)) + { + _cachedKnownColorNames.Add(knownColor, GetDisplayName(knownColor)); + } + } + } + } + + // Find the closest known color by finding nearest Hue + // Since Hue is the best measure of human perception of the color itself + // it is not necessary to check other components (Saturation, Value). var closestKnownColor = KnownColor.None; - var closestKnownColorDistance = double.PositiveInfinity; - var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); + var closestKnownColorHueDiff = double.PositiveInfinity; - for (int i = 1; i < knownColors.Length; i++) // Skip 'None' + lock (_knownColorCacheMutex) { - // Transparent is skipped since alpha is ignored making it equivalent to White - if (knownColors[i] != KnownColor.Transparent) + foreach (var hueEntry in _cachedKnownColorHues) { - Color knownColor = KnownColors.ToColor(knownColors[i]); + // Closest hue before or after is allowed + // Therefore, use an absolute value + double difference = Math.Abs(hsvColor.H - hueEntry.Value); - 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)); - - if (distance < closestKnownColorDistance) + if (difference < closestKnownColorHueDiff) { - closestKnownColor = knownColors[i]; - closestKnownColorDistance = distance; + closestKnownColor = hueEntry.Key; + closestKnownColorHueDiff = difference; } } } @@ -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(roundedColor, displayName); } return displayName; @@ -142,5 +192,35 @@ namespace Avalonia.Controls.Primitives return string.Empty; } } + + /// + /// Gets the human-readable display name for the given . + /// + /// + /// This currently uses the enum value's C# name directly + /// which limits it to the EN language only. In the future this should be localized + /// to other cultures. + /// + /// The to get the display name for. + /// The human-readable display name for the given . + private static string GetDisplayName(KnownColor knownColor) + { + var sb = StringBuilderCache.Acquire(); + string name = knownColor.ToString(); + + // Add spaces converting PascalCase to human-readable names + for (int i = 0; i < name.Length; i++) + { + if (i != 0 && + char.IsUpper(name[i])) + { + sb.Append(' '); + } + + sb.Append(name[i]); + } + + return StringBuilderCache.GetStringAndRelease(sb); + } } } From 1b30b26c48000b972a3c7b6e82aecf237c01167c Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Mar 2023 15:09:13 -0400 Subject: [PATCH 29/58] Improve HsvColor saturation remarks --- src/Avalonia.Base/Media/HsvColor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index f97457c54d..df68252065 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -131,7 +131,7 @@ namespace Avalonia.Media /// /// /// - /// 0 is a shade of gray (no color). + /// 0 is fully white (or a shade of gray) and shows no color. /// 1 is the full color. /// /// From bb8e3ae9ab5173aa4575c2f5cce342e44579fe4d Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Mar 2023 15:11:01 -0400 Subject: [PATCH 30/58] Switch ColorSpectrum.ThirdComponent to a read-only DirectProperty --- .../ColorPicker/ColorPicker.cs | 4 +--- .../ColorSpectrum/ColorSpectrum.Properties.cs | 10 +++++----- .../ColorSpectrum/ColorSpectrum.cs | 9 +++++---- .../ColorView/ColorView.cs | 3 --- .../Helpers/ColorPickerHelpers.cs | 2 -- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 01cb745ba7..92d8535272 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -1,6 +1,4 @@ -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls +namespace Avalonia.Controls { /// /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 5c7de2459b..2245eb8022 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -96,10 +96,10 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty ThirdComponentProperty = - AvaloniaProperty.Register( + public static readonly DirectProperty ThirdComponentProperty = + AvaloniaProperty.RegisterDirect( nameof(ThirdComponent), - ColorComponent.Component3); // Value + o => o.ThirdComponent); /// /// Gets or sets the currently selected color in the RGB color model. @@ -239,8 +239,8 @@ namespace Avalonia.Controls.Primitives /// public ColorComponent ThirdComponent { - get => GetValue(ThirdComponentProperty); - protected set => SetValue(ThirdComponentProperty, value); + get => _thirdComponent; + private set => SetAndRaise(ThirdComponentProperty, ref _thirdComponent, value); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index fde011bc46..9198a2f237 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -13,9 +13,9 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.Reactive; namespace Avalonia.Controls.Primitives { @@ -48,6 +48,7 @@ namespace Avalonia.Controls.Primitives private bool _isPointerPressed = false; private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); + private ColorComponent _thirdComponent = ColorComponent.Component3; // HsvComponent.Value private IDisposable? _layoutRootDisposable; private IDisposable? _selectionEllipsePanelDisposable; @@ -509,15 +510,15 @@ namespace Avalonia.Controls.Primitives { case ColorSpectrumComponents.HueSaturation: case ColorSpectrumComponents.SaturationHue: - SetCurrentValue(ThirdComponentProperty, (ColorComponent)HsvComponent.Value); + ThirdComponent = (ColorComponent)HsvComponent.Value; break; case ColorSpectrumComponents.HueValue: case ColorSpectrumComponents.ValueHue: - SetCurrentValue(ThirdComponentProperty, (ColorComponent)HsvComponent.Saturation); + ThirdComponent = (ColorComponent)HsvComponent.Saturation; break; case ColorSpectrumComponents.SaturationValue: case ColorSpectrumComponents.ValueSaturation: - SetCurrentValue(ThirdComponentProperty, (ColorComponent)HsvComponent.Hue); + ThirdComponent = (ColorComponent)HsvComponent.Hue; break; } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 38be6cfc5b..1d6d5aa008 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -1,14 +1,11 @@ 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 { diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index 819d745772..dbd92d4ac5 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -4,8 +4,6 @@ // Licensed to The Avalonia Project under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Layout; From a92d7abcc7fd2a6162ef3b36f320896596549e14 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Mar 2023 16:09:14 -0400 Subject: [PATCH 31/58] Switch ColorHelper.ToDisplayName() to an HSV color space algorithm that uses all components --- .../Helpers/ColorHelper.cs | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 0c47a8ed3f..7fa2dfdddd 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -11,11 +11,11 @@ namespace Avalonia.Controls.Primitives /// public static class ColorHelper { - private static readonly Dictionary _cachedDisplayNames = new Dictionary(); - private static readonly Dictionary _cachedKnownColorHues = new Dictionary(); + private static readonly Dictionary _cachedDisplayNames = new Dictionary(); private static readonly Dictionary _cachedKnownColorNames = new Dictionary(); private static readonly object _displayNameCacheMutex = new object(); private static readonly object _knownColorCacheMutex = new object(); + private static readonly KnownColor[] _knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); /// /// Gets the relative (perceptual) luminance/brightness of the given color. @@ -62,7 +62,36 @@ namespace Avalonia.Controls.Primitives /// The approximate color display name. public static string ToDisplayName(Color color) { - // Without rounding, there are 16,777,216 possible RGB colors (without alpha). + var hsvColor = color.ToHsv(); + + // Handle extremes that are outside the below algorithm + if (color.A == 0x00) + { + return GetDisplayName(KnownColor.Transparent); + } + + // HSV ---------------------------------------------------------------------- + // + // There are far too many possible HSV colors to cache and search through + // for performance reasons. Therefore, the HSV color is rounded. + // Rounding is tolerable in this algorithm because it is perception based. + // Hue is the most important for user perception so is rounded the least. + // Then there is a lot of loss in rounding the saturation and value components + // which are not as closely related to perceived color. + // + // Hue : Round to nearest int (0..360) + // Saturation : Round to the nearest 1/10 (0..1) + // Value : Round to the nearest 1/10 (0..1) + // Alpha : Is ignored in this algorithm + // + // Rounding results in ~36_000 values to cache in the worse case. + // + // RGB ---------------------------------------------------------------------- + // + // The original algorithm worked in RGB color space. + // If this code is every adjusted to work in RGB again note the following: + // + // Without rounding, there are 16_777_216 possible RGB colors (without alpha). // This is too many to cache and search through for performance reasons. // It is also needlessly large as there are only ~140 known/named colors. // Therefore, rounding of the input color's component values is done to @@ -71,58 +100,29 @@ 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 hsvColor = color.ToHsv(); - - // Handle extremes that are outside the below algorithm - if (color.A == 0x00) - { - return GetDisplayName(KnownColor.Transparent); - } - else if (hsvColor.S <= 0.0) - { - return GetDisplayName(KnownColor.White); - } - else if (hsvColor.V <= 0.0) - { - return GetDisplayName(KnownColor.Black); - } + 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 (_displayNameCacheMutex) { - if (_cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) + if (_cachedDisplayNames.TryGetValue(roundedHsvColor, out var displayName)) { return displayName; } } - // Build KnownColor caches if they don't already exist + // Build the KnownColor name cache if it doesn't already exist lock (_knownColorCacheMutex) { - if (_cachedKnownColorHues.Count == 0 || - _cachedKnownColorNames.Count == 0) + if (_cachedKnownColorNames.Count == 0) { - _cachedKnownColorHues.Clear(); - _cachedKnownColorNames.Clear(); - - var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); - for (int i = 1; i < knownColors.Length; i++) // Skip 'None' so start at 1 + 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 (knownColor == KnownColor.Transparent) - { - continue; - } - - double hue = KnownColors.ToColor(knownColor).ToHsv().H; + KnownColor knownColor = _knownColors[i]; // Some known colors have the same numerical value. For example: // - Aqua = 0xff00ffff @@ -131,11 +131,6 @@ namespace Avalonia.Controls.Primitives // This is not possible to represent in a dictionary which requires // unique values. Therefore, only the first value is used. - if (!_cachedKnownColorHues.ContainsKey(knownColor)) - { - _cachedKnownColorHues.Add(knownColor, hue); - } - if (!_cachedKnownColorNames.ContainsKey(knownColor)) { _cachedKnownColorNames.Add(knownColor, GetDisplayName(knownColor)); @@ -144,24 +139,29 @@ namespace Avalonia.Controls.Primitives } } - // Find the closest known color by finding nearest Hue - // Since Hue is the best measure of human perception of the color itself - // it is not necessary to check other components (Saturation, Value). + // 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 closestKnownColorHueDiff = double.PositiveInfinity; + var closestKnownColorDistance = double.PositiveInfinity; - lock (_knownColorCacheMutex) + for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1 { - foreach (var hueEntry in _cachedKnownColorHues) + KnownColor knownColor = _knownColors[i]; + + // Transparent is skipped since alpha is ignored making it equivalent to White + if (knownColor != KnownColor.Transparent) { - // Closest hue before or after is allowed - // Therefore, use an absolute value - double difference = Math.Abs(hsvColor.H - hueEntry.Value); + HsvColor knownHsvColor = KnownColors.ToColor(knownColor).ToHsv(); + + double distance = Math.Sqrt( + 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 (difference < closestKnownColorHueDiff) + if (distance < closestKnownColorDistance) { - closestKnownColor = hueEntry.Key; - closestKnownColorHueDiff = difference; + closestKnownColor = knownColor; + closestKnownColorDistance = distance; } } } @@ -182,7 +182,7 @@ namespace Avalonia.Controls.Primitives lock (_displayNameCacheMutex) { - _cachedDisplayNames.Add(roundedColor, displayName); + _cachedDisplayNames.Add(roundedHsvColor, displayName); } return displayName; From 9507439b8b65fcee7f16b45ab3b4fa6916c6b91f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 18 Mar 2023 23:25:24 +0100 Subject: [PATCH 32/58] Check for SetCurrentValue when reevaluating. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 53cd3ff307..81206d3065 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -810,10 +810,13 @@ namespace Avalonia.PropertyStore // 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's priority is equal to the current effective value's priority, but the effective + // value was set via SetCurrentValue, 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 == current.Priority && current.IsOverridenCurrentValue) || (priority > BindingPriority.Animation && priority < current.BasePriority); if (foundEntry && isRelevantPriority && entry!.HasValue) From f9ee0e3d8c1339c29714594e6b080ae6a2e30035 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 18 Mar 2023 23:36:22 +0100 Subject: [PATCH 33/58] Another failing test for SetCurrentValue. --- .../AvaloniaObjectTests_SetCurrentValue.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index a7eb95ba40..2cc8b46600 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Concurrency; using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Data; @@ -408,6 +409,43 @@ namespace Avalonia.Base.UnitTests Assert.Equal("new", target.Foo); } + + [Fact] + public void CurrentValue_Is_Replaced_By_New_Style_Activation() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.FooProperty, "initial"), + new Setter(Class1.BarProperty, "bar"), + }, + }, + new Style(x => x.OfType().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); + } private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) { @@ -441,5 +479,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(); + } + } + } + } } } From 23332cd048507ae602d14c935771e8ddd67252ea Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 19 Mar 2023 00:59:57 -0400 Subject: [PATCH 34/58] Support configurable AlphaComponentPosition in ColorView The default now matches CSS and differs from XAML/WinUI. The CSS trailing alpha component is in wider use for end-users now and it also matches the default slider ordering in the UI. --- src/Avalonia.Base/Media/Color.cs | 2 +- .../ColorView/AlphaComponentPosition.cs | 26 +++ .../ColorView/ColorView.Properties.cs | 18 ++ .../ColorView/ColorView.cs | 18 +- .../Converters/ColorToHexConverter.cs | 169 ++++++++++++++++-- .../Themes/Fluent/ColorView.xaml | 1 - .../Themes/Simple/ColorView.xaml | 1 - 7 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 74e70b2a14..f06f272e51 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -309,7 +309,7 @@ namespace Avalonia.Media if (input.Length == 3 || input.Length == 4) { var extendedLength = 2 * input.Length; - + #if !BUILDTASK Span extended = stackalloc char[extendedLength]; #else diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs b/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs new file mode 100644 index 0000000000..4f3ae46a24 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Controls +{ + /// + /// Defines the position of a color's alpha component relative to all other components. + /// + public enum AlphaComponentPosition + { + /// + /// The alpha component occurs before all other components. + /// + /// + /// For example, this may indicate the #AARRGGBB or ARGB format which + /// is the default format for XAML itself and the Color struct. + /// + Leading, + + /// + /// The alpha component occurs after all other components. + /// + /// + /// For example, this may indicate the #RRGGBBAA or RGBA format which + /// is the default format for CSS. + /// + Trailing, + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index b76059037b..e334a1d323 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -42,6 +42,14 @@ namespace Avalonia.Controls nameof(ColorSpectrumShape), ColorSpectrumShape.Box); + /// + /// Defines the property. + /// + public static readonly StyledProperty HexInputAlphaPositionProperty = + AvaloniaProperty.Register( + nameof(HexInputAlphaPosition), + AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI + /// /// Defines the property. /// @@ -260,6 +268,16 @@ namespace Avalonia.Controls set => SetValue(ColorSpectrumShapeProperty, value); } + /// + /// Gets or sets the position of the alpha component in the hexadecimal input box relative to + /// all other color components. + /// + public AlphaComponentPosition HexInputAlphaPosition + { + get => GetValue(HexInputAlphaPositionProperty); + set => SetValue(HexInputAlphaPositionProperty, value); + } + /// public HsvColor HsvColor { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 1d6d5aa008..274e7f5851 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -25,8 +24,7 @@ namespace Avalonia.Controls private TextBox? _hexTextBox; private TabControl? _tabControl; - private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); - protected bool ignorePropertyChanged = false; + protected bool _ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -43,7 +41,7 @@ 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) { @@ -63,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); } } @@ -197,7 +195,7 @@ namespace Avalonia.Controls /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (ignorePropertyChanged) + if (_ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -206,7 +204,7 @@ namespace Avalonia.Controls // Always keep the two color properties in sync if (change.Property == ColorProperty) { - ignorePropertyChanged = true; + _ignorePropertyChanged = true; SetCurrentValue(HsvColorProperty, Color.ToHsv()); SetColorToHexTextBox(); @@ -215,11 +213,11 @@ namespace Avalonia.Controls change.GetOldValue(), change.GetNewValue())); - ignorePropertyChanged = false; + _ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - ignorePropertyChanged = true; + _ignorePropertyChanged = true; SetCurrentValue(ColorProperty, HsvColor.ToRgb()); SetColorToHexTextBox(); @@ -228,7 +226,7 @@ namespace Avalonia.Controls change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - ignorePropertyChanged = false; + _ignorePropertyChanged = false; } else if (change.Property == PaletteProperty) { diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 8d5f2332be..8798f874f4 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -2,6 +2,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls.Converters { @@ -10,6 +11,11 @@ namespace Avalonia.Controls.Converters /// public class ColorToHexConverter : IValueConverter { + /// + /// Gets or sets the position of a color's alpha component relative to all other components. + /// + public AlphaComponentPosition AlphaPosition { get; set; } = AlphaComponentPosition.Leading; + /// public object? Convert( object? value, @@ -42,16 +48,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); - - if (includeSymbol == false) - { - // TODO: When .net standard 2.0 is dropped, replace the below line - //hexColor = hexColor.Replace("#", string.Empty, StringComparison.Ordinal); - hexColor = hexColor.Replace("#", string.Empty); - } - - return hexColor; + return ToHexString(color, AlphaPosition, includeSymbol); } /// @@ -62,21 +59,159 @@ namespace Avalonia.Controls.Converters CultureInfo culture) { string hexValue = value?.ToString() ?? string.Empty; + return ParseHexString(hexValue, AlphaPosition) ?? AvaloniaProperty.UnsetValue; + } + + /// + /// Converts the given color to its hex color value string representation. + /// + /// The color to represent as a hex value string. + /// The output position of the alpha component. + /// Whether the hex symbol '#' will be added. + /// The input color converted to its hex value string. + public static string ToHexString( + Color color, + AlphaComponentPosition alphaPosition, + bool includeSymbol = false) + { + uint intColor; + if (alphaPosition == AlphaComponentPosition.Trailing) + { + intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + } + else + { + // Default is Leading alpha + intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + } - if (Color.TryParse(hexValue, out Color color)) + string hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); + + if (includeSymbol) + { + hexColor = '#' + hexColor; + } + + return hexColor; + } + + /// + /// Parses a hex color value string into a new . + /// + /// The hex color string to parse. + /// The input position of the alpha component. + /// The parsed ; otherwise, null. + public static Color? ParseHexString( + string hexColor, + AlphaComponentPosition alphaPosition) + { + hexColor = hexColor.Trim(); + + if (!hexColor.StartsWith("#", StringComparison.Ordinal)) + { + hexColor = "#" + hexColor; + } + + if (TryParseHexFormat(hexColor.AsSpan(), alphaPosition, out Color color)) { return color; } - else if (hexValue.StartsWith("#", StringComparison.Ordinal) == false && - Color.TryParse("#" + hexValue, out Color color2)) + + return null; + } + + /// + /// Parses the given span of characters representing a hex color value into a new . + /// + /// + /// This is based on the Color.TryParseHexFormat() method. + /// It is copied because it needs to be extended to handle alpha position. + /// However, the alpha position enum is only available in the controls namespace with the ColorPicker control. + /// + private static bool TryParseHexFormat( + ReadOnlySpan s, + AlphaComponentPosition alphaPosition, + out Color color) + { + static bool TryParseCore(ReadOnlySpan input, AlphaComponentPosition alphaPosition, ref Color color) { - return color2; + var alphaComponent = 0u; + + if (input.Length == 6) + { + if (alphaPosition == AlphaComponentPosition.Trailing) + { + alphaComponent = 0x000000FF; + } + else + { + alphaComponent = 0xFF000000; + } + } + else if (input.Length != 8) + { + return false; + } + + if (!input.TryParseUInt(NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed)) + { + return false; + } + + if (alphaComponent != 0) + { + if (alphaPosition == AlphaComponentPosition.Trailing) + { + parsed = (parsed << 8) | alphaComponent; + } + else + { + parsed = parsed | alphaComponent; + } + } + + if (alphaPosition == AlphaComponentPosition.Trailing) + { + // #RRGGBBAA + color = new Color( + a: (byte)(parsed & 0xFF), + r: (byte)((parsed >> 24) & 0xFF), + g: (byte)((parsed >> 16) & 0xFF), + b: (byte)((parsed >> 8) & 0xFF)); + } + else + { + // #AARRGGBB + color = new Color( + a: (byte)((parsed >> 24) & 0xFF), + r: (byte)((parsed >> 16) & 0xFF), + g: (byte)((parsed >> 8) & 0xFF), + b: (byte)(parsed & 0xFF)); + } + + return true; } - else + + color = default; + + ReadOnlySpan input = s.Slice(1); + + // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB). + if (input.Length == 3 || input.Length == 4) { - // Invalid hex color value provided - return AvaloniaProperty.UnsetValue; + var extendedLength = 2 * input.Length; + Span extended = stackalloc char[extendedLength]; + + for (int i = 0; i < input.Length; i++) + { + extended[2 * i + 0] = input[i]; + extended[2 * i + 1] = input[i]; + } + + return TryParseCore(extended, alphaPosition, ref color); } + + return TryParseCore(input, alphaPosition, ref color); } } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index a7d84441aa..f72fb11bbe 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -8,7 +8,6 @@ - 48 diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index ab7288dd8f..4e219a98af 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -8,7 +8,6 @@ - 48 From 290ab36d84e549b292d58849e4548c24ecee6114 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 14:26:47 +0300 Subject: [PATCH 35/58] Reset the signaled state before executing jobs --- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 4360d8111d..9db8c5e8c0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -94,15 +94,10 @@ public partial class Dispatcher private void Signaled() { - try - { - ExecuteJobsCore(); - } - finally - { - lock (InstanceLock) - _signaled = false; - } + lock (InstanceLock) + _signaled = false; + + ExecuteJobsCore(); } void ExecuteJobsCore() From ee445f21976e9d40166e59ebd2866e4fa2a9ff10 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 15:54:35 +0600 Subject: [PATCH 36/58] Fixed dispatcher for macOS --- native/Avalonia.Native/src/OSX/platformthreading.mm | 4 +--- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 8 +++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index b727b9a6cf..d2d7a365a6 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -79,13 +79,11 @@ static double distantFutureInterval = (double)50*365*24*3600; bool signaled; @synchronized (self) { signaled = self->_signaled; + self->_signaled = false; } if(signaled) { self->_events->Signaled(); - @synchronized (self) { - self->_signaled = false; - } } }); CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 9db8c5e8c0..0c5414e6d1 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -124,7 +124,13 @@ public partial class Dispatcher else if (_pendingInputImpl?.CanQueryPendingInput == true) { if (!_pendingInputImpl.HasPendingInput) - ExecuteJob(job); + { + // On platforms like macOS HasPendingInput check might trigger a timer + // which would result in reentrancy here, so we check if the job + // hasn't been executed yet + if (job.Status == DispatcherOperationStatus.Pending) + ExecuteJob(job); + } else { RequestBackgroundProcessing(); From 13bbdc729e026a7c4e6ec8ce33f5d83c9f24648f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 19:52:17 +0600 Subject: [PATCH 37/58] Use deadline-based and platform-implemented background processing for macOS --- .../src/OSX/platformthreading.mm | 67 ++++++++++++++----- .../Threading/Dispatcher.Queue.cs | 27 +++++--- src/Avalonia.Base/Threading/Dispatcher.cs | 4 ++ .../Threading/IDispatcherImpl.cs | 12 +++- src/Avalonia.Native/DispatcherImpl.cs | 10 ++- src/Avalonia.Native/avn.idl | 3 +- 6 files changed, 93 insertions(+), 30 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index d2d7a365a6..d80df68fea 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -66,25 +66,44 @@ static double distantFutureInterval = (double)50*365*24*3600; ComPtr _events; bool _wakeupDelegateSent; bool _signaled; + bool _backgroundProcessingRequested; CFRunLoopObserverRef _observer; CFRunLoopTimerRef _timer; } +- (void) checkSignaled +{ + bool signaled; + @synchronized (self) { + signaled = _signaled; + _signaled = false; + } + if(signaled) + { + _events->Signaled(); + } +} + - (Signaler*) init { _observer = CFRunLoopObserverCreateWithHandler(nil, - kCFRunLoopBeforeSources | kCFRunLoopAfterWaiting, + kCFRunLoopBeforeSources + | kCFRunLoopAfterWaiting + | kCFRunLoopBeforeWaiting + , true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - bool signaled; - @synchronized (self) { - signaled = self->_signaled; - self->_signaled = false; - } - if(signaled) + if(activity == kCFRunLoopBeforeWaiting) { - self->_events->Signaled(); + bool triggerProcessing; + @synchronized (self) { + triggerProcessing = self->_backgroundProcessingRequested; + self->_backgroundProcessingRequested = false; + } + if(triggerProcessing) + self->_events->ReadyForBackgroundProcessing(); } + [self checkSignaled]; }); CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); @@ -135,10 +154,27 @@ static double distantFutureInterval = (double)50*365*24*3600; if(_signaled) return; _signaled = true; + dispatch_async(dispatch_get_main_queue(), ^{ + [self checkSignaled]; + }); CFRunLoopWakeUp(CFRunLoopGetMain()); } } +- (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 @@ -165,15 +201,7 @@ public: return [NSThread isMainThread]; }; - bool HasPendingInput() override - { - auto event = [NSApp - nextEventMatchingMask: NSEventMaskAny - untilDate:nil - inMode:NSDefaultRunLoopMode - dequeue:false]; - return event != nil; - }; + void SetEvents(IAvnPlatformThreadingInterfaceEvents *cb) override { @@ -227,6 +255,11 @@ public: [_signaler updateTimer:ms]; }; + void RequestBackgroundProcessing() override { + [_signaler requestBackgroundProcessing]; + } + + }; extern IAvnPlatformThreadingInterface* CreatePlatformThreading() diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 0c5414e6d1..105019f277 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -7,13 +7,21 @@ 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 (_dueTimeForBackgroundProcessing == null) + if (_backgroundProcessingImpl != null) + { + if(_explicitBackgroundProcessingRequested) + return; + _explicitBackgroundProcessingRequested = true; + _backgroundProcessingImpl.RequestBackgroundProcessing(); + } + else if (_dueTimeForBackgroundProcessing == null) { _dueTimeForBackgroundProcessing = Clock.TickCount + 1; UpdateOSTimer(); @@ -21,6 +29,15 @@ public partial class Dispatcher } } + private void OnReadyForExplicitBackgroundProcessing() + { + lock (InstanceLock) + { + _explicitBackgroundProcessingRequested = false; + ExecuteJobsCore(); + } + } + /// /// Force-runs all dispatcher operations ignoring any pending OS events, use with caution /// @@ -124,13 +141,7 @@ public partial class Dispatcher else if (_pendingInputImpl?.CanQueryPendingInput == true) { if (!_pendingInputImpl.HasPendingInput) - { - // On platforms like macOS HasPendingInput check might trigger a timer - // which would result in reentrancy here, so we check if the job - // hasn't been executed yet - if (job.Status == DispatcherOperationStatus.Pending) - ExecuteJob(job); - } + ExecuteJob(job); else { RequestBackgroundProcessing(); diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 9a0dacfbb8..d1bd15e286 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -23,6 +23,7 @@ public partial class Dispatcher : IDispatcher private IControlledDispatcherImpl? _controlledImpl; private static Dispatcher? s_uiThread; private IDispatcherImplWithPendingInput? _pendingInputImpl; + private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; internal Dispatcher(IDispatcherImpl impl, IDispatcherClock clock) { @@ -32,6 +33,9 @@ public partial class Dispatcher : IDispatcher 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(); diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 7f1e352b51..2cc06d1986 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -1,9 +1,11 @@ using System; using System.Threading; +using Avalonia.Metadata; using Avalonia.Platform; namespace Avalonia.Threading; +[Unstable] public interface IDispatcherImpl { bool CurrentThreadIsLoopThread { get; } @@ -15,7 +17,7 @@ public interface IDispatcherImpl void UpdateTimer(int? dueTimeInMs); } - +[Unstable] public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can @@ -24,6 +26,14 @@ public interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } +[Unstable] +public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl +{ + event Action ReadyForBackgroundProcessing; + void RequestBackgroundProcessing(); +} + +[Unstable] public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop diff --git a/src/Avalonia.Native/DispatcherImpl.cs b/src/Avalonia.Native/DispatcherImpl.cs index a9e5e6deb1..b1d3cb59de 100644 --- a/src/Avalonia.Native/DispatcherImpl.cs +++ b/src/Avalonia.Native/DispatcherImpl.cs @@ -10,7 +10,7 @@ using MicroCom.Runtime; namespace Avalonia.Native; -internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock +internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDispatcherImplWithExplicitBackgroundProcessing { private readonly IAvnPlatformThreadingInterface _native; private Thread? _loopThread; @@ -25,6 +25,7 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public event Action Signaled; public event Action Timer; + public event Action ReadyForBackgroundProcessing; private class Events : NativeCallbackBase, IAvnPlatformThreadingInterfaceEvents { @@ -37,6 +38,8 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public void Signaled() => _parent.Signaled?.Invoke(); public void Timer() => _parent.Timer?.Invoke(); + + public void ReadyForBackgroundProcessing() => _parent.ReadyForBackgroundProcessing?.Invoke(); } public bool CurrentThreadIsLoopThread @@ -60,8 +63,8 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock _native.UpdateTimer(ms); } - public bool CanQueryPendingInput => true; - public bool HasPendingInput => _native.HasPendingInput() != 0; + public bool CanQueryPendingInput => false; + public bool HasPendingInput => false; class RunLoopFrame : IDisposable { @@ -124,4 +127,5 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock frame.Exception = capture; frame.CancellationTokenSource.Cancel(); } + public void RequestBackgroundProcessing() => _native.RequestBackgroundProcessing(); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 7763d0d2fc..09e9168d8f 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -649,6 +649,7 @@ interface IAvnPlatformThreadingInterfaceEvents : IUnknown { void Signaled(); void Timer(); + void ReadyForBackgroundProcessing(); } [uuid(97330f88-c22b-4a8e-a130-201520091b01)] @@ -661,12 +662,12 @@ interface IAvnLoopCancellation : IUnknown interface IAvnPlatformThreadingInterface : IUnknown { bool GetCurrentThreadIsLoopThread(); - bool HasPendingInput(); void SetEvents(IAvnPlatformThreadingInterfaceEvents* cb); IAvnLoopCancellation* CreateLoopCancellation(); void RunLoop(IAvnLoopCancellation* cancel); void Signal(); void UpdateTimer(int ms); + void RequestBackgroundProcessing(); } [uuid(6c621a6e-e4c1-4ae3-9749-83eeeffa09b6)] From 0b69ab1a3c1d5d98b3ceb425c4c0684e5e5899be Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 19 Mar 2023 11:57:23 -0400 Subject: [PATCH 38/58] Allow derived controls access to Slider template parts and state --- src/Avalonia.Controls/Slider.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 7de726a932..b7f7566e21 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -86,10 +86,10 @@ namespace Avalonia.Controls TickBar.TicksProperty.AddOwner(); // 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; From 927e5ff77da1437669405e9cf2d506addbc76cea Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 19 Mar 2023 11:58:10 -0400 Subject: [PATCH 39/58] Calculate the ColorSlider spectrum based on the track size --- .../ColorSlider/ColorSlider.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 7cbadcdf6d..ce47a797ec 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -96,8 +96,22 @@ namespace Avalonia.Controls.Primitives // independent pixels of controls. var scale = LayoutHelper.GetLayoutScale(this); - var pixelWidth = Convert.ToInt32(Bounds.Width * scale); - var pixelHeight = Convert.ToInt32(Bounds.Height * scale); + int pixelWidth; + int pixelHeight; + + if (base._track != null) + { + pixelWidth = Convert.ToInt32(base._track.Bounds.Width * scale); + pixelHeight = Convert.ToInt32(base._track.Bounds.Height * scale); + } + else + { + // As a fallback, attempt to calculate using the overall control size + // This shouldn't happen as a track is a required template part of a slider + // However, if it does, the spectrum will still be shown + pixelWidth = Convert.ToInt32(Bounds.Width * scale); + pixelHeight = Convert.ToInt32(Bounds.Height * scale); + } if (pixelWidth != 0 && pixelHeight != 0) { From dbbdcb95cd0ceb76f777620fa12473e764890726 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Mar 2023 22:19:55 +0600 Subject: [PATCH 40/58] Refactored dispatcher clock --- .../Threading/Dispatcher.Queue.cs | 11 ++++---- .../Threading/Dispatcher.Timers.cs | 18 +++++++------ src/Avalonia.Base/Threading/Dispatcher.cs | 6 ++--- .../Threading/DispatcherTimer.cs | 6 ++--- .../Threading/IDispatcherClock.cs | 13 ---------- .../Threading/IDispatcherImpl.cs | 16 ++++++++---- .../Platform/ManagedDispatcherImpl.cs | 3 ++- src/Avalonia.Native/DispatcherImpl.cs | 9 ++++--- src/Avalonia.X11/X11PlatformThreading.cs | 6 ++--- .../Avalonia.Win32/Win32DispatcherImpl.cs | 10 ++++--- .../DispatcherTests.cs | 26 +++++++++---------- 11 files changed, 61 insertions(+), 63 deletions(-) delete mode 100644 src/Avalonia.Base/Threading/IDispatcherClock.cs diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 105019f277..c91af1a514 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -23,7 +23,7 @@ public partial class Dispatcher } else if (_dueTimeForBackgroundProcessing == null) { - _dueTimeForBackgroundProcessing = Clock.TickCount + 1; + _dueTimeForBackgroundProcessing = Now + 1; UpdateOSTimer(); } } @@ -68,7 +68,8 @@ public partial class Dispatcher public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => 0; + public void UpdateTimer(long? dueTimeInMs) { } } @@ -119,7 +120,7 @@ public partial class Dispatcher void ExecuteJobsCore() { - int? backgroundJobExecutionStartedAt = null; + long? backgroundJobExecutionStartedAt = null; while (true) { DispatcherOperation? job; @@ -153,9 +154,9 @@ public partial class Dispatcher else { if (backgroundJobExecutionStartedAt == null) - backgroundJobExecutionStartedAt = Clock.TickCount; + backgroundJobExecutionStartedAt = Now; - if (Clock.TickCount - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) + if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) { _signaled = true; RequestBackgroundProcessing(); diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index 269d10707e..bb252b7f55 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -9,11 +9,13 @@ public partial class Dispatcher private List _timers = new(); private long _timersVersion; private bool _dueTimeFound; - private int _dueTimeInMs; + private long _dueTimeInMs; - private int? _dueTimeForTimers; - private int? _dueTimeForBackgroundProcessing; - private int? _osTimerSetTo; + private long? _dueTimeForTimers; + private long? _dueTimeForBackgroundProcessing; + private long? _osTimerSetTo; + + internal long Now => _impl.Now; private void UpdateOSTimer() { @@ -40,7 +42,7 @@ public partial class Dispatcher if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read { bool oldDueTimeFound = _dueTimeFound; - int oldDueTimeInTicks = _dueTimeInMs; + long oldDueTimeInTicks = _dueTimeInMs; _dueTimeFound = false; _dueTimeInMs = 0; @@ -113,11 +115,11 @@ public partial class Dispatcher lock (InstanceLock) { _impl.UpdateTimer(_osTimerSetTo = null); - needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Clock.TickCount; + needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now; if (needToPromoteTimers) _dueTimeForTimers = null; needToProcessQueue = _dueTimeForBackgroundProcessing.HasValue && - _dueTimeForBackgroundProcessing.Value <= Clock.TickCount; + _dueTimeForBackgroundProcessing.Value <= Now; if (needToProcessQueue) _dueTimeForBackgroundProcessing = null; } @@ -131,7 +133,7 @@ public partial class Dispatcher internal void PromoteTimers() { - int currentTimeInTicks = Clock.TickCount; + long currentTimeInTicks = Now; try { List? timers = null; diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index d1bd15e286..25a4a4ce2c 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -17,7 +17,6 @@ namespace Avalonia.Threading; public partial class Dispatcher : IDispatcher { private IDispatcherImpl _impl; - internal IDispatcherClock Clock { get; } internal object InstanceLock { get; } = new(); private bool _hasShutdownFinished; private IControlledDispatcherImpl? _controlledImpl; @@ -25,10 +24,9 @@ public partial class Dispatcher : IDispatcher private IDispatcherImplWithPendingInput? _pendingInputImpl; private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; - internal Dispatcher(IDispatcherImpl impl, IDispatcherClock clock) + internal Dispatcher(IDispatcherImpl impl) { _impl = impl; - Clock = clock; impl.Timer += OnOSTimer; impl.Signaled += Signaled; _controlledImpl = _impl as IControlledDispatcherImpl; @@ -51,7 +49,7 @@ public partial class Dispatcher : IDispatcher else impl = new NullDispatcherImpl(); } - return new Dispatcher(impl, impl as IDispatcherClock ?? new DefaultDispatcherClock()); + return new Dispatcher(impl); } /// diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 0c235ee161..879d9d8a5f 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -125,7 +125,7 @@ public partial class DispatcherTimer if (_isEnabled) { - DueTimeInMs = _dispatcher.Clock.TickCount + (int)_interval.TotalMilliseconds; + DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds; updateOSTimer = true; } } @@ -288,7 +288,7 @@ public partial class DispatcherTimer // BeginInvoke a new operation. _operation = _dispatcher.InvokeAsync(FireTick, DispatcherPriority.Inactive); - DueTimeInMs = _dispatcher.Clock.TickCount + (int)_interval.TotalMilliseconds; + DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds; if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess()) { @@ -348,5 +348,5 @@ public partial class DispatcherTimer private bool _isEnabled; // used by Dispatcher - internal int DueTimeInMs { get; private set; } + internal long DueTimeInMs { get; private set; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/IDispatcherClock.cs b/src/Avalonia.Base/Threading/IDispatcherClock.cs deleted file mode 100644 index 2a5268d192..0000000000 --- a/src/Avalonia.Base/Threading/IDispatcherClock.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Avalonia.Threading; - -internal interface IDispatcherClock -{ - int TickCount { get; } -} - -internal class DefaultDispatcherClock : IDispatcherClock -{ - public int TickCount => Environment.TickCount; -} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 2cc06d1986..670ec55461 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using Avalonia.Metadata; using Avalonia.Platform; @@ -14,7 +15,8 @@ public interface IDispatcherImpl void Signal(); event Action Signaled; event Action Timer; - void UpdateTimer(int? dueTimeInMs); + long Now { get; } + void UpdateTimer(long? dueTimeInMs); } [Unstable] @@ -40,10 +42,11 @@ public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput void RunLoop(CancellationToken token); } -internal class LegacyDispatcherImpl : DefaultDispatcherClock, IControlledDispatcherImpl +internal class LegacyDispatcherImpl : IControlledDispatcherImpl { private readonly IPlatformThreadingInterface _platformThreading; private IDisposable? _timer; + private Stopwatch _clock = Stopwatch.StartNew(); public LegacyDispatcherImpl(IPlatformThreadingInterface platformThreading) { @@ -56,14 +59,15 @@ internal class LegacyDispatcherImpl : DefaultDispatcherClock, IControlledDispatc public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => _clock.ElapsedMilliseconds; + public void UpdateTimer(long? dueTimeInMs) { _timer?.Dispose(); _timer = null; if (dueTimeInMs.HasValue) { - var interval = Math.Max(1, dueTimeInMs.Value - TickCount); + var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds); _timer = _platformThreading.StartTimer(DispatcherPriority.Send, TimeSpan.FromMilliseconds(interval), OnTick); @@ -94,7 +98,9 @@ class NullDispatcherImpl : IDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => 0; + + public void UpdateTimer(long? dueTimeInMs) { } diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index 54c96113ea..20aa91c83e 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -40,7 +40,8 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl public event Action? Signaled; public event Action? Timer; - public void UpdateTimer(int? dueTimeInMs) + public long Now => _clock.ElapsedMilliseconds; + public void UpdateTimer(long? dueTimeInMs) { lock (_lock) { diff --git a/src/Avalonia.Native/DispatcherImpl.cs b/src/Avalonia.Native/DispatcherImpl.cs index b1d3cb59de..fd8ef567f4 100644 --- a/src/Avalonia.Native/DispatcherImpl.cs +++ b/src/Avalonia.Native/DispatcherImpl.cs @@ -10,10 +10,11 @@ using MicroCom.Runtime; namespace Avalonia.Native; -internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDispatcherImplWithExplicitBackgroundProcessing +internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing { private readonly IAvnPlatformThreadingInterface _native; private Thread? _loopThread; + private Stopwatch _clock = Stopwatch.StartNew(); private Stack _managedFrames = new(); public DispatcherImpl(IAvnPlatformThreadingInterface native) @@ -57,9 +58,9 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDi public void Signal() => _native.Signal(); - public void UpdateTimer(int? dueTimeInMs) + public void UpdateTimer(long? dueTimeInMs) { - var ms = dueTimeInMs == null ? -1 : Math.Max(1, dueTimeInMs.Value - TickCount); + var ms = dueTimeInMs == null ? -1 : (int)Math.Min(int.MaxValue - 10, Math.Max(1, dueTimeInMs.Value - Now)); _native.UpdateTimer(ms); } @@ -113,7 +114,7 @@ internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock, IDi } } - public int TickCount => Environment.TickCount; + public long Now => _clock.ElapsedMilliseconds; public void PropagateCallbackException(ExceptionDispatchInfo capture) { diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/X11PlatformThreading.cs index f2f45bce8e..de0e3bee5d 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/X11PlatformThreading.cs @@ -9,7 +9,7 @@ using static Avalonia.X11.XLib; namespace Avalonia.X11 { - internal unsafe class X11PlatformThreading : IControlledDispatcherImpl, IDispatcherClock + internal unsafe class X11PlatformThreading : IControlledDispatcherImpl { private readonly AvaloniaX11Platform _platform; private readonly IntPtr _display; @@ -227,7 +227,7 @@ namespace Avalonia.X11 public event Action Signaled; public event Action Timer; - public void UpdateTimer(int? dueTimeInMs) + public void UpdateTimer(long? dueTimeInMs) { _nextTimer = dueTimeInMs; if (_nextTimer != null) @@ -235,7 +235,7 @@ namespace Avalonia.X11 } - public int TickCount => (int)_clock.ElapsedMilliseconds; + public long Now => (int)_clock.ElapsedMilliseconds; public bool CanQueryPendingInput => true; public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0; diff --git a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs index 3c2f7842ba..581e5fa306 100644 --- a/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs +++ b/src/Windows/Avalonia.Win32/Win32DispatcherImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Threading; @@ -6,10 +7,11 @@ using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32; -internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock +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; @@ -36,7 +38,7 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock public void FireTimer() => Timer?.Invoke(); - public void UpdateTimer(int? dueTimeInMs) + public void UpdateTimer(long? dueTimeInMs) { if (dueTimeInMs == null) { @@ -44,7 +46,7 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock } else { - var interval = (uint)Math.Max(1, TickCount - dueTimeInMs.Value); + var interval = (uint)Math.Min(int.MaxValue - 10, Math.Max(1, Now - dueTimeInMs.Value)); SetTimer( _messageWindow, (IntPtr)Win32Platform.TIMERID_DISPATCHER, @@ -115,5 +117,5 @@ internal class Win32DispatcherImpl : IControlledDispatcherImpl, IDispatcherClock } } - public int TickCount => Environment.TickCount; + public long Now => _clock.ElapsedMilliseconds; } diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index 902af94121..38175ad410 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -7,7 +7,7 @@ namespace Avalonia.Base.UnitTests; public class DispatcherTests { - class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherClock, IDispatcherImplWithPendingInput + class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput { public bool CurrentThreadIsLoopThread => true; @@ -15,15 +15,15 @@ public class DispatcherTests public event Action Signaled; public event Action Timer; - public int? NextTimer { get; private set; } + public long? NextTimer { get; private set; } public bool AskedForSignal { get; private set; } - public void UpdateTimer(int? dueTimeInTicks) + public void UpdateTimer(long? dueTimeInTicks) { NextTimer = dueTimeInTicks; } - public int TickCount { get; set; } + public long Now { get; set; } public void ExecuteSignal() { @@ -37,7 +37,7 @@ public class DispatcherTests { if (NextTimer == null) return; - TickCount = NextTimer.Value; + Now = NextTimer.Value; Timer?.Invoke(); } @@ -51,7 +51,7 @@ public class DispatcherTests public void DispatcherExecutesJobsAccordingToPriority() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background); disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render); @@ -65,7 +65,7 @@ public class DispatcherTests public void DispatcherPreservesOrderWhenChangingPriority() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background); var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input); @@ -84,7 +84,7 @@ public class DispatcherTests public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); for (var c = 0; c < 10; c++) { @@ -92,7 +92,7 @@ public class DispatcherTests disp.Post(() => { actions.Add(itemId); - impl.TickCount += 20; + impl.Now += 20; }, DispatcherPriority.Background); } @@ -114,7 +114,7 @@ public class DispatcherTests Assert.False(impl.AskedForSignal); if (c < 3) { - Assert.True(impl.NextTimer > impl.TickCount); + Assert.True(impl.NextTimer > impl.Now); } else Assert.Null(impl.NextTimer); @@ -127,7 +127,7 @@ public class DispatcherTests { var impl = new SimpleDispatcherImpl(); impl.TestInputPending = true; - var disp = new Dispatcher(impl, impl); + var disp = new Dispatcher(impl); var actions = new List(); for (var c = 0; c < 10; c++) { @@ -160,8 +160,8 @@ public class DispatcherTests Assert.False(impl.AskedForSignal); if (c < 3) { - Assert.True(impl.NextTimer > impl.TickCount); - impl.TickCount = impl.NextTimer.Value + 1; + Assert.True(impl.NextTimer > impl.Now); + impl.Now = impl.NextTimer.Value + 1; } else Assert.Null(impl.NextTimer); From 0bdd79b964c62a9192b68aed9b1bbab6b9bf96fd Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 19 Mar 2023 12:40:33 -0400 Subject: [PATCH 41/58] Update ColorSlider ControlTheme 1. Support lightweight resources to change track and control size independently 2. Make the background spectrum follow the track size --- .../Themes/Fluent/ColorSlider.xaml | 67 +++++++++++-------- .../Themes/Simple/ColorSlider.xaml | 67 +++++++++++-------- 2 files changed, 76 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index 7e25bd8051..789473571b 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -1,13 +1,20 @@  + + 20 + 20 + 10 + 10 + - + @@ -25,27 +32,28 @@ - + diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs index c53891fa1c..c3630c36b7 100644 --- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Base.UnitTests.Utilities; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.GestureRecognizers; diff --git a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj index a99421f107..17e1ab0e50 100644 --- a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj +++ b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index a0d8f8bcc3..4732099d60 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -17,6 +17,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Threading; +using Avalonia.UnitTests; using Avalonia.Utilities; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index 0d182678ef..ca9f5ed974 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs b/tests/Avalonia.UnitTests/DispatcherTimerHelper.cs similarity index 76% rename from src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs rename to tests/Avalonia.UnitTests/DispatcherTimerHelper.cs index a457388fb2..ef7271a2d3 100644 --- a/src/Avalonia.Base/Utilities/DispatcherTimerHelper.cs +++ b/tests/Avalonia.UnitTests/DispatcherTimerHelper.cs @@ -1,11 +1,6 @@ using Avalonia.Threading; -namespace Avalonia.Utilities; - -public class DispatcherTimerHelper -{ - -} +namespace Avalonia.UnitTests; public static class DispatcherTimerUtils { From 637f04ac3adb6b67e50934a9b1bc2f675c43ea01 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Mar 2023 15:11:36 +0600 Subject: [PATCH 56/58] Make DispatcherPriority values more in line with WPF --- .../Threading/DispatcherPriority.cs | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index 9d0b91f6f2..a4386370f0 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -16,64 +16,64 @@ namespace Avalonia.Threading { Value = value; } - - /// - /// Minimum possible priority that's actually dispatched, default value - /// - internal static readonly DispatcherPriority MinimumActiveValue = new(0); /// - /// A dispatcher priority for jobs that shouldn't be executed yet + /// The lowest foreground dispatcher priority /// - public static DispatcherPriority Inactive => new(MinimumActiveValue - 1); + internal static readonly DispatcherPriority Default = new(0); + /// - /// Minimum valid priority + /// The job will be processed with the same priority as input. /// - internal static readonly DispatcherPriority MinValue = new(Inactive); + public static readonly DispatcherPriority Input = new(Default - 1); /// - /// Used internally in dispatcher code + /// The job will be processed after other non-idle operations have completed. /// - public static DispatcherPriority Invalid => new(MinimumActiveValue - 2); + public static readonly DispatcherPriority Background = new(Input - 1); + /// + /// The job will be processed after background operations have completed. + /// + public static readonly DispatcherPriority ContextIdle = new(Background - 1); /// - /// The job will be processed when the system is idle. + /// The job will be processed when the application is idle. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinimumActiveValue; - + public static readonly DispatcherPriority ApplicationIdle = new (ContextIdle - 1); + /// - /// The job will be processed when the application is idle. + /// The job will be processed when the system is idle. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = new (SystemIdle + 1); + public static readonly DispatcherPriority SystemIdle = new(ApplicationIdle - 1); /// - /// The job will be processed after background operations have completed. + /// Minimum possible priority that's actually dispatched, default value /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = new(ApplicationIdle + 1); + internal static readonly DispatcherPriority MinimumActiveValue = new(SystemIdle); + /// - /// The job will be processed with normal priority. + /// A dispatcher priority for jobs that shouldn't be executed yet /// -#pragma warning disable CS0618 - public static readonly DispatcherPriority Normal = new(ContextIdle + 1); -#pragma warning restore CS0618 - + public static DispatcherPriority Inactive => new(MinimumActiveValue - 1); + /// - /// The job will be processed after other non-idle operations have completed. + /// Minimum valid priority /// - public static readonly DispatcherPriority Background = new(MinValue + 1); - + internal static readonly DispatcherPriority MinValue = new(Inactive); + /// - /// The job will be processed with the same priority as input. + /// Used internally in dispatcher code /// - public static readonly DispatcherPriority Input = new(Background + 1); - + public static DispatcherPriority Invalid => new(MinimumActiveValue - 2); + + /// /// The job will be processed after layout and render but before input. /// - public static readonly DispatcherPriority Loaded = new(Input + 1); + public static readonly DispatcherPriority Loaded = new(Default + 1); /// /// The job will be processed with the same priority as render. @@ -98,12 +98,19 @@ namespace Avalonia.Threading /// /// The job will be processed with the same priority as data binding. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue; + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = new(Layout); + + /// + /// The job will be processed with normal priority. + /// +#pragma warning disable CS0618 + public static readonly DispatcherPriority Normal = new(DataBind + 1); +#pragma warning restore CS0618 /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(Layout + 1); + public static readonly DispatcherPriority Send = new(Normal + 1); /// /// Maximum possible priority From e3a75b869bba63fccec09062b8d7dcbcae44e34e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Mar 2023 15:23:56 +0600 Subject: [PATCH 57/58] QoL --- .../Threading/DispatcherPriority.cs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a4386370f0..a71140d288 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -57,7 +57,7 @@ namespace Avalonia.Threading /// /// A dispatcher priority for jobs that shouldn't be executed yet /// - public static DispatcherPriority Inactive => new(MinimumActiveValue - 1); + public static readonly DispatcherPriority Inactive = new(MinimumActiveValue - 1); /// /// Minimum valid priority @@ -67,7 +67,7 @@ namespace Avalonia.Threading /// /// Used internally in dispatcher code /// - public static DispatcherPriority Invalid => new(MinimumActiveValue - 2); + public static readonly DispatcherPriority Invalid = new(MinimumActiveValue - 2); /// @@ -158,5 +158,42 @@ namespace Avalonia.Threading 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 } } From 530f176783693e7f06b70d9f8d366ed5e7b50cb5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Mar 2023 21:18:01 +0600 Subject: [PATCH 58/58] Added Rgb24 and Bgr24 --- .../Media/Imaging/PixelFormatReaders.cs | 46 ++++++++++++++++++- src/Avalonia.Base/Platform/PixelFormat.cs | 8 +++- .../Avalonia.RenderTests/Media/BitmapTests.cs | 2 + 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs index fc7c174ed6..c90c4cb5ac 100644 --- a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs @@ -228,6 +228,44 @@ static unsafe class PixelFormatReader public void Reset(IntPtr address) => _address = (Rgba64*)address; } + + public unsafe struct Rgb24PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + _address += 3; + return new Rgba8888Pixel + { + R = addr[0], + G = addr[1], + B = addr[2], + A = 255, + }; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgr24PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + _address += 3; + return new Rgba8888Pixel + { + R = addr[2], + G = addr[1], + B = addr[0], + A = 255, + }; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst, PixelFormat format) @@ -242,6 +280,10 @@ static unsafe class PixelFormatReader Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Gray16) Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Rgb24) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Bgr24) + Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Gray32Float) Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Rgba64) @@ -258,7 +300,9 @@ static unsafe class PixelFormatReader || format == PixelFormats.Gray8 || format == PixelFormats.Gray16 || format == PixelFormats.Gray32Float - || format == PixelFormats.Rgba64; + || format == PixelFormats.Rgba64 + || format == PixelFormats.Bgr24 + || format == PixelFormats.Rgb24; } public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader diff --git a/src/Avalonia.Base/Platform/PixelFormat.cs b/src/Avalonia.Base/Platform/PixelFormat.cs index 99fe17055d..95f49bdb25 100644 --- a/src/Avalonia.Base/Platform/PixelFormat.cs +++ b/src/Avalonia.Base/Platform/PixelFormat.cs @@ -13,7 +13,9 @@ namespace Avalonia.Platform Gray8, Gray16, Gray32Float, - Rgba64 + Rgba64, + Rgb24, + Bgr24 } public record struct PixelFormat @@ -35,6 +37,8 @@ namespace Avalonia.Platform else if (FormatEnum == PixelFormatEnum.Rgb565 || FormatEnum == PixelFormatEnum.Gray16) return 16; + else if (FormatEnum is PixelFormatEnum.Bgr24 or PixelFormatEnum.Rgb24) + return 24; else if (FormatEnum == PixelFormatEnum.Rgba64) return 64; @@ -70,5 +74,7 @@ namespace Avalonia.Platform public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8); public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16); public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); + public static PixelFormat Rgb24 { get; } = new PixelFormat(PixelFormatEnum.Rgb24); + public static PixelFormat Bgr24 { get; } = new PixelFormat(PixelFormatEnum.Bgr24); } } diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 4ba0c82b87..6916d0c130 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -143,6 +143,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media InlineData(PixelFormatEnum.Gray4), InlineData(PixelFormatEnum.Gray8), InlineData(PixelFormatEnum.Gray16), + InlineData(PixelFormatEnum.Rgb24), + InlineData(PixelFormatEnum.Bgr24), InlineData(PixelFormatEnum.Gray32Float), InlineData(PixelFormatEnum.Rgba64), InlineData(PixelFormatEnum.Rgba64, AlphaFormat.Premul),