From 7311ed01b68ba942f2f5929550630d71542ebae8 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 22 Feb 2023 20:14:57 -0500 Subject: [PATCH 001/196] 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 002/196] 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 007/196] 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 008/196] 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 009/196] 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 010/196] 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 011/196] 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 012/196] 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 013/196] 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 014/196] 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 015/196] 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 016/196] 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 017/196] 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 018/196] 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 019/196] 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 020/196] 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 021/196] 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 022/196] 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 023/196] 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 024/196] 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 025/196] 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 026/196] 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 027/196] 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 028/196] 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 029/196] 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 030/196] 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 031/196] 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 032/196] 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 033/196] 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 034/196] 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 035/196] 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 036/196] 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 037/196] 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 038/196] 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 039/196] 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 055/196] 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 056/196] 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 fe1f9d4c77ba5e3f723e5b3ee6e44bc425332b85 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 23 Mar 2023 09:43:30 +0000 Subject: [PATCH 057/196] add a failing unit test. --- .../TreeViewTests.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 4f533c2f78..fbcfd99743 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1091,6 +1091,46 @@ namespace Avalonia.Controls.UnitTests Assert.True(called); } } + + [Fact] + public void SelectedItem_Should_Be_Valid_When_SelectedItemChanged_Event_Raised() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + ItemsSource = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.TreeContainerFromItem(item); + + Assert.NotNull(container); + + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Same(item, e.AddedItems[0]); + Assert.Same(item, target.SelectedItem); + called = true; + }; + + _mouse.Click(container); + + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + Assert.True(called); + } + } [Fact] public void Bound_SelectedItem_Should_Not_Be_Cleared_when_Changing_Selection() From e77de043b6286ffbe19b85fede130f693e54f483 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 23 Mar 2023 09:46:11 +0000 Subject: [PATCH 058/196] [TreeView] ensure SelectedItem field is set before Adding items to SelectedItems --- src/Avalonia.Controls/TreeView.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8f2636a783..194ba37671 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -303,12 +303,14 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { + var oldValue = _selectedItem; _syncingSelectedItems = true; - SelectedItems.Clear(); + SelectedItems.Clear(); + _selectedItem = item; SelectedItems.Add(item); _syncingSelectedItems = false; - SetAndRaise(SelectedItemProperty, ref _selectedItem, item); + RaisePropertyChanged(SelectedItemProperty, oldValue, _selectedItem); } /// From 1e84029a41d56c4cec40d94ec3de2d8fab9c5aee Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Mar 2023 02:37:08 -0400 Subject: [PATCH 059/196] Downgrade Microsoft.CodeAnalysis.CSharp --- src/tools/Avalonia.Generators/Avalonia.Generators.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj index 4ebece583c..88128d6b45 100644 --- a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj +++ b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj @@ -9,7 +9,7 @@ - + From 530f176783693e7f06b70d9f8d366ed5e7b50cb5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Mar 2023 21:18:01 +0600 Subject: [PATCH 060/196] 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), From f307b7364a164f16c8585cd0a169512f2380e490 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 24 Mar 2023 17:33:37 +0100 Subject: [PATCH 061/196] Fix TextLineImp GetTextBounds Fix Tab character handling --- .../Media/TextFormatting/TextLineImpl.cs | 17 +++--------- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 6 ++--- .../Media/TextShaperImpl.cs | 13 +++++++--- .../Media/TextFormatting/TextLayoutTests.cs | 26 ++++++++++++++++++- .../Media/TextFormatting/TextLineTests.cs | 24 +++++++++++++++++ .../Media/TextFormatting/TextShaperTests.cs | 6 ++--- .../HarfBuzzTextShaperImpl.cs | 12 ++++++--- 7 files changed, 75 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f426a20b2c..f522ffb5c3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -697,13 +697,8 @@ namespace Avalonia.Media.TextFormatting i = lastRunIndex; - if (directionalWidth == 0) - { - continue; - } - - var coveredLength = 0; - TextBounds? textBounds = null; + int coveredLength; + TextBounds? textBounds; switch (currentDirection) { @@ -831,14 +826,8 @@ namespace Avalonia.Media.TextFormatting i = firstRunIndex; - if (directionalWidth == 0) - { - continue; - } - - var coveredLength = 0; - TextBounds? textBounds = null; + int coveredLength; switch (currentDirection) { diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 7d7be46c72..aeb9025ade 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -26,8 +26,8 @@ namespace Avalonia.Skia using (var buffer = new Buffer()) { // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length); - buffer.AddUtf16(containingText.Span, start, length); + var containingText = GetContainingMemory(text, out var start, out var length).Span; + buffer.AddUtf16(containingText, start, length); MergeBreakPair(buffer); @@ -72,7 +72,7 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (i < textSpan.Length && textSpan[i] == '\t') + if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 36897d3aff..b881129488 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; @@ -13,6 +14,8 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { + private static readonly ConcurrentDictionary s_cachedLanguage = new(); + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; @@ -24,8 +27,8 @@ namespace Avalonia.Direct2D1.Media using (var buffer = new Buffer()) { // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length); - buffer.AddUtf16(containingText.Span, start, length); + var containingText = GetContainingMemory(text, out var start, out var length).Span; + buffer.AddUtf16(containingText, start, length); MergeBreakPair(buffer); @@ -33,7 +36,9 @@ namespace Avalonia.Direct2D1.Media buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + var usedCulture = culture ?? CultureInfo.CurrentCulture; + + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); var font = ((GlyphTypefaceImpl)typeface).Font; @@ -68,7 +73,7 @@ namespace Avalonia.Direct2D1.Media var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (i < textSpan.Length && textSpan[i] == '\t') + if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index a24da38fdd..c84fcaaa9a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1051,7 +1051,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")] [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")] [Theory] - public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, + public void Should_HitTestTextRangeBetweenRuns(string text, int start, int length, FlowDirection flowDirection, string expected) { using (Start()) @@ -1087,6 +1087,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_HitTestTextRangeWithLineBreaks() + { + using (Start()) + { + var beforeLinebreak = "Line before linebreak"; + var afterLinebreak = "Line after linebreak"; + var text = beforeLinebreak + Environment.NewLine + "" + Environment.NewLine + afterLinebreak; + + var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black); + + var end = text.Length - afterLinebreak.Length + 1; + + var rects = textLayout.HitTestTextRange(0, end).ToArray(); + + Assert.Equal(3, rects.Length); + + var endX = textLayout.TextLines[2].GetDistanceFromCharacterHit(new CharacterHit(end)); + + //First character should be covered + Assert.Equal(7.201171875, endX, 2); + } + } + private static IDisposable Start() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 0cfa78d007..0ddcb2452d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -622,6 +622,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Get_TextBounds_For_LineBreak() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(Environment.NewLine, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textBounds = textLine.GetTextBounds(0, Environment.NewLine.Length); + + Assert.Equal(1, textBounds.Count); + + Assert.Equal(1, textBounds[0].TextRunBounds.Count); + + Assert.Equal(Environment.NewLine.Length, textBounds[0].TextRunBounds[0].Length); + } + } + [Fact] public void Should_GetTextRange() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 92778793c2..b748f8fc58 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -31,11 +31,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\t"; + var text = "012345\t"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); + var shapedBuffer = TextShaper.Current.ShapeText(text.AsMemory().Slice(6), options); - Assert.Equal(shapedBuffer.Length, text.Length); + Assert.Equal(1, shapedBuffer.Length); Assert.Equal(100, shapedBuffer[0].GlyphAdvance); } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 375f87259d..98f188f15a 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; @@ -13,6 +14,7 @@ namespace Avalonia.UnitTests { internal class HarfBuzzTextShaperImpl : ITextShaperImpl { + private static readonly ConcurrentDictionary s_cachedLanguage = new(); public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; @@ -24,8 +26,8 @@ namespace Avalonia.UnitTests using (var buffer = new Buffer()) { // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length); - buffer.AddUtf16(containingText.Span, start, length); + var containingText = GetContainingMemory(text, out var start, out var length).Span; + buffer.AddUtf16(containingText, start, length); MergeBreakPair(buffer); @@ -33,7 +35,9 @@ namespace Avalonia.UnitTests buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + var usedCulture = culture ?? CultureInfo.CurrentCulture; + + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font; @@ -68,7 +72,7 @@ namespace Avalonia.UnitTests var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (textSpan[i] == '\t') + if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); From 27b2b2733001b3aeee68264d67d8479447fb5aa0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 24 Mar 2023 18:14:46 +0100 Subject: [PATCH 062/196] Make ExpandSubTree work again. --- src/Avalonia.Controls/TreeView.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8f2636a783..1a9042a25b 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Layout; using Avalonia.Threading; using Avalonia.VisualTree; @@ -163,6 +164,9 @@ namespace Avalonia.Controls { item.IsExpanded = true; + if (item.Presenter?.Panel is null) + (this.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + if (item.Presenter?.Panel is { } panel) { foreach (var child in panel.Children) From a3866201a4ecc56262e7c61072f9056ae91d1df3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 24 Mar 2023 18:02:25 +0100 Subject: [PATCH 063/196] Modernized TreeViewTests. --- .../TreeViewTests.cs | 1975 ++++++----------- 1 file changed, 721 insertions(+), 1254 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 4f533c2f78..9e70e16924 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1,43 +1,35 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using Avalonia.Collections; -using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; -using Moq; +using Avalonia.VisualTree; using Xunit; +#nullable enable + namespace Avalonia.Controls.UnitTests { public class TreeViewTests { - MouseTestHelper _mouse = new MouseTestHelper(); + private readonly MouseTestHelper _mouse = new(); [Fact] public void Items_Should_Be_Created() { - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = CreateTestTreeData(), - }; - - var root = new TestRoot(target); - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + using var app = Start(); + var target = CreateTarget(); Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); @@ -47,50 +39,31 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Items_Should_Be_Created_Using_ItemTemplate_If_Present() { - TreeView target; - - var root = new TestRoot - { - Child = target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = CreateTestTreeData(), - ItemTemplate = new FuncTreeDataTemplate( - (_, __) => new Canvas(), - x => x.Children), - } - }; - - ApplyTemplates(target); + using var app = Start(); + var itemTemplate = new FuncTreeDataTemplate( + (_, _) => new Canvas(), + x => x.Children); + var target = CreateTarget(itemTemplate: itemTemplate); var items = target.GetRealizedTreeContainers() .OfType() .ToList(); Assert.Equal(5, items.Count); - Assert.All(items, x => Assert.IsType(x.HeaderPresenter.Child)); + Assert.All(items, x => Assert.IsType(x.HeaderPresenter?.Child)); } [Fact] public void Items_Should_Be_Created_Using_ItemConatinerTheme_If_Present() { - TreeView target; - var theme = new ControlTheme(typeof(TreeViewItem)); - - var root = new TestRoot - { - Child = target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = CreateTestTreeData(), - ItemContainerTheme = theme, - ItemTemplate = new FuncTreeDataTemplate( - (_, __) => new Canvas(), - x => x.Children), - } - }; + var theme = CreateTreeViewItemControlTheme(); + var itemTemplate = new FuncTreeDataTemplate( + (_, _) => new Canvas(), + x => x.Children); - ApplyTemplates(target); + var target = CreateTarget( + itemContainerTheme: theme, + itemTemplate: itemTemplate); var items = target.GetRealizedTreeContainers() .OfType() @@ -104,21 +77,12 @@ namespace Avalonia.Controls.UnitTests public void Finds_Correct_DataTemplate_When_Application_DataTemplate_Is_Present() { // #10398 - using var app = UnitTestApplication.Start(); - - Avalonia.Application.Current.DataTemplates.Add(new FuncDataTemplate((x, _) => new Canvas())); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(Avalonia.Application.Current); + using var app = Start(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = CreateTestTreeData(), - }; - - var root = new TestRoot(target); + Avalonia.Application.Current!.DataTemplates.Add(new FuncDataTemplate((x, _) => new Canvas())); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Avalonia.Application.Current); - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var target = CreateTarget(); Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); @@ -128,46 +92,27 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() { - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = CreateTestTreeData(), - }; - - var root = new TestRoot(target); - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + using var app = Start(); + var target = CreateTarget(); var container = (TreeViewItem)target.GetRealizedContainers().Single(); - var header = (TextBlock)container.HeaderPresenter.Child; - Assert.Equal("Root", header.Text); + var header = Assert.IsType(container.HeaderPresenter?.Child); + Assert.Equal("Root", header?.Text); } [Fact] public void Root_TreeContainerFromItem_Should_Return_Descendant_Item() { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - // For TreeViewItem to find its parent TreeView, OnAttachedToLogicalTree needs - // to be called, which requires an IStyleRoot. - var root = new TestRoot(); - root.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - var container = target.TreeContainerFromItem(tree[0].Children[1].Children[0]); + var container = target.TreeContainerFromItem(data[0].Children[1].Children[0]); Assert.NotNull(container); - var header = ((TreeViewItem)container).HeaderPresenter; - var headerContent = ((TextBlock)header.Child).Text; + var header = ((TreeViewItem)container).HeaderPresenter!; + var headerContent = ((TextBlock)header.Child!).Text; Assert.Equal("Grandchild2a", headerContent); } @@ -175,335 +120,192 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Select_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - Assert.NotNull(container); + var item = data[0].Children[1].Children[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); - _mouse.Click(container); + _mouse.Click(container); - Assert.Equal(item, target.SelectedItem); - Assert.True(container.IsSelected); - } + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - Assert.NotNull(container); + var item = data[0].Children[1].Children[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); - _mouse.Click(container, modifiers: KeyModifiers.Control); + _mouse.Click(container, modifiers: KeyModifiers.Control); - Assert.Null(target.SelectedItem); - Assert.False(container.IsSelected); - } + Assert.Null(target.SelectedItem); + Assert.False(container.IsSelected); } [Fact] public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var item1 = tree[0].Children[1].Children[0]; - var container1 = (TreeViewItem)target.TreeContainerFromItem(item1); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - var item2 = tree[0].Children[1]; - var container2 = (TreeViewItem)target.TreeContainerFromItem(item2); + var item1 = data[0].Children[1].Children[0]; + var container1 = Assert.IsType(target.TreeContainerFromItem(item1)); - Assert.NotNull(container1); - Assert.NotNull(container2); + var item2 = data[0].Children[1]; + var container2 = Assert.IsType(target.TreeContainerFromItem(item2)); - target.SelectedItem = item1; + target.SelectedItem = item1; - Assert.True(container1.IsSelected); + Assert.True(container1.IsSelected); - _mouse.Click(container2, modifiers: KeyModifiers.Control); + _mouse.Click(container2, modifiers: KeyModifiers.Control); - Assert.Equal(item2, target.SelectedItem); - Assert.False(container1.IsSelected); - Assert.True(container2.IsSelected); - } + Assert.Equal(item2, target.SelectedItem); + Assert.False(container1.IsSelected); + Assert.True(container2.IsSelected); } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var rootNode = tree[0]; - - var item1 = rootNode.Children[0]; - var item2 = rootNode.Children.Last(); - - var item1Container = (TreeViewItem)target.TreeContainerFromItem(item1); - var item2Container = (TreeViewItem)target.TreeContainerFromItem(item2); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var item1 = rootNode.Children[0]; + var item2 = rootNode.Children.Last(); + var item1Container = Assert.IsType(target.TreeContainerFromItem(item1)); + var item2Container = Assert.IsType(target.TreeContainerFromItem(item2)); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.True(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.True(item1Container.IsSelected); - ClickContainer(item2Container, KeyModifiers.Control); - Assert.True(item2Container.IsSelected); + ClickContainer(item2Container, KeyModifiers.Control); + Assert.True(item2Container.IsSelected); - Assert.Equal(new[] { item1, item2 }, target.SelectedItems.OfType()); + Assert.Equal(new[] { item1, item2 }, target.SelectedItems.OfType()); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.False(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.False(item1Container.IsSelected); - Assert.DoesNotContain(item1, target.SelectedItems.OfType()); - } + Assert.DoesNotContain(item1, target.SelectedItems.OfType()); } [Fact] public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var rootNode = tree[0]; - - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); - - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(from); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); - - ClickContainer(fromContainer, KeyModifiers.None); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); + var fromContainer = Assert.IsType(target.TreeContainerFromItem(from)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); - Assert.True(fromContainer.IsSelected); + ClickContainer(fromContainer, KeyModifiers.None); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); - } + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var rootNode = tree[0]; - - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; - - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(from); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); - - ClickContainer(fromContainer, KeyModifiers.None); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; + var fromContainer = Assert.IsType(target.TreeContainerFromItem(from)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); - Assert.True(fromContainer.IsSelected); + ClickContainer(fromContainer, KeyModifiers.None); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); - } + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var rootNode = tree[0]; - - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; - - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(from); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); - - ClickContainer(fromContainer, KeyModifiers.None); - - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); - - ClickContainer(fromContainer, KeyModifiers.None); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; + var fromContainer = Assert.IsType(target.TreeContainerFromItem(from)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); - Assert.True(fromContainer.IsSelected); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); - foreach (var child in rootNode.Children) - { - if (child == from) - { - continue; - } + ClickContainer(fromContainer, KeyModifiers.None); + Assert.True(fromContainer.IsSelected); - var container = (TreeViewItem)target.TreeContainerFromItem(child); + foreach (var child in rootNode.Children) + { + if (child == from) + continue; - Assert.False(container.IsSelected); - } + var container = Assert.IsType(target.TreeContainerFromItem(child)); + Assert.False(container.IsSelected); } } [Fact] public void Double_Clicking_Item_Header_Should_Expand_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + CollapseAll(target); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - CollapseAll(target); + var item = data[0].Children[1]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - var item = tree[0].Children[1]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + Assert.False(container.IsExpanded); + Assert.NotNull(header); - Assert.NotNull(container); - Assert.False(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + _mouse.DoubleClick(header); - _mouse.DoubleClick(header); - - Assert.True(container.IsExpanded); - } + Assert.True(container.IsExpanded); } [Fact] public void Double_Clicking_Item_Header_With_No_Children_Does_Not_Expand_It() { - using (Application()) + using var app = Start(); { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - CreateNodeDataTemplate(target); - ApplyTemplates(target); CollapseAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + var item = data[0].Children[1].Children[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - Assert.NotNull(container); Assert.False(container.IsExpanded); - var header = container.HeaderPresenter.Child; Assert.NotNull(header); _mouse.DoubleClick(header); @@ -515,346 +317,238 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Double_Clicking_Item_Header_Should_Collapse_It() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var item = tree[0].Children[1]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var item = data[0].Children[1]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - Assert.NotNull(container); - Assert.True(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + Assert.True(container.IsExpanded); + Assert.NotNull(header); - _mouse.DoubleClick(header); + _mouse.DoubleClick(header); - Assert.False(container.IsExpanded); - } + Assert.False(container.IsExpanded); } [Fact] public void Enter_Key_Should_Collapse_TreeViewItem() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); // NOTE this line - - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - Assert.NotNull(container); - Assert.True(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + Assert.True(container.IsExpanded); + Assert.NotNull(header); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Enter, - }); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); - Assert.False(container.IsExpanded); - } + Assert.False(container.IsExpanded); } [Fact] - public void Enter_plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively() + public void Enter_Plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); // NOTE this line + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + Assert.True(container.IsExpanded); + Assert.NotNull(header); - Assert.NotNull(container); - Assert.True(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Enter, - KeyModifiers = KeyModifiers.Control, - }); + Assert.False(container.IsExpanded); - Assert.False(container.IsExpanded); + AssertEachItemWithChildrenIsCollapsed(item); - AssertEachItemWithChildrenIsCollapsed(item); + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = Assert.IsType(target.TreeContainerFromItem(node)); - void AssertEachItemWithChildrenIsCollapsed(Node node) + if (node.Children?.Count > 0) { - var container = (TreeViewItem)target.TreeContainerFromItem(node); - Assert.NotNull(container); - if (node.Children?.Count > 0) - { - Assert.False(container.IsExpanded); - foreach (var c in node.Children) - { - AssertEachItemWithChildrenIsCollapsed(c); - } - } - else + Assert.False(container.IsExpanded); + foreach (var c in node.Children) { - Assert.True(container.IsExpanded); + AssertEachItemWithChildrenIsCollapsed(c); } } + else + { + Assert.True(container.IsExpanded); + } } } [Fact] public void Enter_Key_Should_Expand_TreeViewItem() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + CollapseAll(target); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - CollapseAll(target); // NOTE this line + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + Assert.False(container.IsExpanded); + Assert.NotNull(header); - Assert.NotNull(container); - Assert.False(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); - - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Enter, - }); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); - Assert.True(container.IsExpanded); - } + Assert.True(container.IsExpanded); } [Fact] - public void Enter_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively() + public void Enter_Plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + CollapseAll(target); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - CollapseAll(target); // NOTE this line + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + Assert.False(container.IsExpanded); + Assert.NotNull(header); - Assert.NotNull(container); - Assert.False(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Enter, - KeyModifiers = KeyModifiers.Control, - }); + Assert.True(container.IsExpanded); - Assert.True(container.IsExpanded); + AssertEachItemWithChildrenIsExpanded(item); - AssertEachItemWithChildrenIsExpanded(item); + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = Assert.IsType(target.TreeContainerFromItem(node)); - void AssertEachItemWithChildrenIsExpanded(Node node) + if (node.Children?.Count > 0) { - var container = (TreeViewItem)target.TreeContainerFromItem(node); - Assert.NotNull(container); - if (node.Children?.Count > 0) + Assert.True(container.IsExpanded); + foreach (var c in node.Children) { - Assert.True(container.IsExpanded); - foreach (var c in node.Children) - { - AssertEachItemWithChildrenIsExpanded(c); - } - } - else - { - Assert.False(container.IsExpanded); + AssertEachItemWithChildrenIsExpanded(c); } } + else + { + Assert.False(container.IsExpanded); + } } } [Fact] public void Space_Key_Should_Collapse_TreeViewItem() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); // NOTE this line - - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - Assert.NotNull(container); - Assert.True(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + Assert.True(container.IsExpanded); + Assert.NotNull(header); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Enter, - }); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); - Assert.False(container.IsExpanded); - } + Assert.False(container.IsExpanded); } [Fact] - public void Space_plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively() + public void Space_Plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); // NOTE this line - - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - Assert.NotNull(container); - Assert.True(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + Assert.True(container.IsExpanded); + Assert.NotNull(header); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Enter, - KeyModifiers = KeyModifiers.Control, - }); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); - Assert.False(container.IsExpanded); + Assert.False(container.IsExpanded); - AssertEachItemWithChildrenIsCollapsed(item); + AssertEachItemWithChildrenIsCollapsed(item); - void AssertEachItemWithChildrenIsCollapsed(Node node) + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = Assert.IsType(target.TreeContainerFromItem(node)); + Assert.NotNull(container); + if (node.Children?.Count > 0) { - var container = (TreeViewItem)target.TreeContainerFromItem(node); - Assert.NotNull(container); - if (node.Children?.Count > 0) + Assert.False(container.IsExpanded); + foreach (var c in node.Children) { - Assert.False(container.IsExpanded); - foreach (var c in node.Children) - { - AssertEachItemWithChildrenIsCollapsed(c); - } - } - else - { - Assert.True(container.IsExpanded); + AssertEachItemWithChildrenIsCollapsed(c); } } + else + { + Assert.True(container.IsExpanded); + } } } [Fact] public void Space_Key_Should_Expand_TreeViewItem() { - using (Application()) + using var app = Start(); { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - CollapseAll(target); // NOTE this line + CollapseAll(target); - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - Assert.NotNull(container); Assert.False(container.IsExpanded); - var header = container.HeaderPresenter.Child; Assert.NotNull(header); container.RaiseEvent(new KeyEventArgs @@ -870,281 +564,204 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Space_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - CollapseAll(target); // NOTE this line + CollapseAll(target); - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); + var header = container.HeaderPresenter?.Child; - Assert.NotNull(container); - Assert.False(container.IsExpanded); - var header = container.HeaderPresenter.Child; - Assert.NotNull(header); + Assert.False(container.IsExpanded); + Assert.NotNull(header); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Enter, - KeyModifiers = KeyModifiers.Control, - }); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); - Assert.True(container.IsExpanded); + Assert.True(container.IsExpanded); - AssertEachItemWithChildrenIsExpanded(item); + AssertEachItemWithChildrenIsExpanded(item); - void AssertEachItemWithChildrenIsExpanded(Node node) + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = Assert.IsType(target.TreeContainerFromItem(node)); + Assert.NotNull(container); + if (node.Children?.Count > 0) { - var container = (TreeViewItem)target.TreeContainerFromItem(node); - Assert.NotNull(container); - if (node.Children?.Count > 0) + Assert.True(container.IsExpanded); + foreach (var c in node.Children) { - Assert.True(container.IsExpanded); - foreach (var c in node.Children) - { - AssertEachItemWithChildrenIsExpanded(c); - } - } - else - { - Assert.False(container.IsExpanded); + AssertEachItemWithChildrenIsExpanded(c); } } + else + { + Assert.False(container.IsExpanded); + } } } [Fact] public void Numpad_Star_Should_Expand_All_Children_Recursively() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - CollapseAll(target); + CollapseAll(target); - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); - Assert.NotNull(container); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Multiply, - }); + Assert.NotNull(container); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Multiply, + }); - AssertEachItemWithChildrenIsExpanded(item); + AssertEachItemWithChildrenIsExpanded(item); - void AssertEachItemWithChildrenIsExpanded(Node node) + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = Assert.IsType(target.TreeContainerFromItem(node)); + Assert.NotNull(container); + if (node.Children?.Count > 0) { - var container = (TreeViewItem)target.TreeContainerFromItem(node); - Assert.NotNull(container); - if (node.Children?.Count > 0) + Assert.True(container.IsExpanded); + foreach (var c in node.Children) { - Assert.True(container.IsExpanded); - foreach (var c in node.Children) - { - AssertEachItemWithChildrenIsExpanded(c); - } - } - else - { - Assert.False(container.IsExpanded); + AssertEachItemWithChildrenIsExpanded(c); } } + else + { + Assert.False(container.IsExpanded); + } } } [Fact] public void Numpad_Slash_Should_Collapse_All_Children_Recursively() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var item = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + Assert.NotNull(container); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Divide, + }); - var item = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + AssertEachItemWithChildrenIsCollapsed(item); + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = Assert.IsType(target.TreeContainerFromItem(node)); Assert.NotNull(container); - container.RaiseEvent(new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = Key.Divide, - }); - - AssertEachItemWithChildrenIsCollapsed(item); - - void AssertEachItemWithChildrenIsCollapsed(Node node) + if (node.Children?.Count > 0) { - var container = (TreeViewItem)target.TreeContainerFromItem(node); - Assert.NotNull(container); - if (node.Children?.Count > 0) + Assert.False(container.IsExpanded); + foreach (var c in node.Children) { - Assert.False(container.IsExpanded); - foreach (var c in node.Children) - { - AssertEachItemWithChildrenIsCollapsed(c); - } - } - else - { - Assert.True(container.IsExpanded); + AssertEachItemWithChildrenIsCollapsed(c); } } + else + { + Assert.True(container.IsExpanded); + } } } [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var item = data[0].Children[1].Children[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); - } + Assert.True(container.IsSelected); } [Fact] public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var item = data[0].Children[1].Children[0]; - var item = tree[0].Children[1].Children[0]; + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.RemovedItems); + Assert.Equal(1, e.AddedItems.Count); + Assert.Same(item, e.AddedItems[0]); + called = true; + }; - var called = false; - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.RemovedItems); - Assert.Equal(1, e.AddedItems.Count); - Assert.Same(item, e.AddedItems[0]); - called = true; - }; - - target.SelectedItem = item; - Assert.True(called); - } + target.SelectedItem = item; + Assert.True(called); } [Fact] public void Bound_SelectedItem_Should_Not_Be_Cleared_when_Changing_Selection() { - using (Application()) - { - var dataContext = new TestDataContext(); - - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - DataContext = dataContext - }; - - target.Bind(TreeView.ItemsSourceProperty, new Binding("Items")); - target.Bind(TreeView.SelectedItemProperty, new Binding("SelectedItem")); + using var app = Start(); + var dataContext = new TestDataContext(); + var target = CreateTarget(); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + target.DataContext = dataContext; + target.Bind(TreeView.ItemsSourceProperty, new Binding("Items")); + target.Bind(TreeView.SelectedItemProperty, new Binding("SelectedItem")); - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var selectedValues = new List(); - var selectedValues = new List(); + dataContext.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(TestDataContext.SelectedItem)) + selectedValues.Add(dataContext.SelectedItem); + }; - dataContext.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(TestDataContext.SelectedItem)) - selectedValues.Add(dataContext.SelectedItem); - }; - selectedValues.Add(dataContext.SelectedItem); + selectedValues.Add(dataContext.SelectedItem); - _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Left); - _mouse.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Left); + _mouse.Click(target.Presenter!.Panel!.Children[0], MouseButton.Left); + _mouse.Click(target.Presenter!.Panel!.Children[2], MouseButton.Left); - Assert.Equal(3, selectedValues.Count); - Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); - } + Assert.Equal(3, selectedValues.Count); + Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); } [Fact] public void LogicalChildren_Should_Be_Set() { - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = new[] { "Foo", "Bar", "Baz " }, - }; + using var app = Start(); + var target = CreateTarget(data: null); - ApplyTemplates(target); + target.ItemsSource = new[] { "Foo", "Bar", "Baz " }; + Layout(target); var result = target.GetLogicalChildren() .OfType() - .Select(x => x.HeaderPresenter.Child) + .Select(x => x.HeaderPresenter?.Child) .OfType() .Select(x => x.Text) .ToList(); @@ -1155,6 +772,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void DataContexts_Should_Be_Correctly_Set() { + using var app = Start(); var items = new object[] { "Foo", @@ -1163,20 +781,14 @@ namespace Avalonia.Controls.UnitTests new TreeViewItem { Header = "Qux" }, }; - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - DataContext = "Base", - DataTemplates = - { - new FuncDataTemplate((x, _) => new Button { Content = x }) - }, - ItemsSource = items, - }; + var target = CreateTarget(); + var root = Assert.IsType(target.GetVisualRoot()); - ApplyTemplates(target); + root.DataTemplates.Add(new FuncDataTemplate((x, _) => new Button { Content = x })); + target.DataContext = "Base"; + target.ItemsSource = items; - var dataContexts = target.Presenter.Panel.Children + var dataContexts = target.Presenter!.Panel!.Children .Cast() .Select(x => x.DataContext) .ToList(); @@ -1189,37 +801,22 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Item_Should_Not_Be_NameScope() { - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = - { - new TreeViewItem(), - } - }; + using var app = Start(); + var target = CreateTarget(data: null); + var item = new TreeViewItem(); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + target.Items!.Add(item); - var item = target.LogicalChildren[0]; + Assert.Same(item, target.LogicalChildren[0]); Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); } [Fact] public void Should_React_To_Children_Changing() { + using var app = Start(); var data = CreateTestTreeData(); - - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = data, - }; - - var root = new TestRoot(target); - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var target = CreateTarget(data: data); Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); @@ -1236,7 +833,7 @@ namespace Avalonia.Controls.UnitTests } }; - ApplyTemplates(target); + Layout(target); Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1)); @@ -1245,374 +842,240 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() { - using (Application()) - { - var focus = FocusManager.Instance; - var navigation = AvaloniaLocator.Current.GetRequiredService(); - var data = CreateTestTreeData(); + using var app = Start(); + var focus = FocusManager.Instance!; + var navigation = AvaloniaLocator.Current.GetRequiredService(); + var data = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = data, - }; + var target = new TreeView + { + ItemsSource = data, + }; - var button = new Button(); + var button = new Button(); - var root = new TestRoot - { - Child = new StackPanel - { - Children = { target, button }, - } - }; + var root = CreateRoot(new StackPanel + { + Children = { target, button }, + }); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + ExpandAll(target); - var item = data[0].Children[0]; - var node = target.TreeContainerFromItem(item); - Assert.NotNull(node); + var item = data[0].Children[0]; + var node = target.TreeContainerFromItem(item); + Assert.NotNull(node); - target.SelectedItem = item; - node.Focus(); - Assert.Same(node, focus.Current); + target.SelectedItem = item; + node.Focus(); + Assert.Same(node, focus.Current); - navigation.Move(focus.Current, NavigationDirection.Next); - Assert.Same(button, focus.Current); + navigation.Move(focus.Current!, NavigationDirection.Next); + Assert.Same(button, focus.Current); - navigation.Move(focus.Current, NavigationDirection.Next); - Assert.Same(node, focus.Current); - } + navigation.Move(focus.Current!, NavigationDirection.Next); + Assert.Same(node, focus.Current); } [Fact] public void Keyboard_Navigation_Should_Not_Crash_If_Selected_Item_Is_not_In_Tree() { - using (Application()) - { - var focus = FocusManager.Instance; - var data = CreateTestTreeData(); - - var selectedNode = new Node { Value = "Out of Tree Selected Item" }; + using var app = Start(); + var focus = FocusManager.Instance!; + var data = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = data, - SelectedItem = selectedNode - }; + var selectedNode = new Node { Value = "Out of Tree Selected Item" }; - var button = new Button(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + ItemsSource = data, + SelectedItem = selectedNode + }; - var root = new TestRoot - { - Child = new StackPanel - { - Children = { target, button }, - } - }; + var button = new Button(); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + var root = CreateRoot(new StackPanel + { + Children = { target, button }, + }); - var item = data[0].Children[0]; - var node = target.TreeContainerFromItem(item); - Assert.NotNull(node); + root.LayoutManager.ExecuteInitialLayoutPass(); + ExpandAll(target); - target.SelectedItem = selectedNode; - node.Focus(); - Assert.Same(node, focus.Current); + var item = data[0].Children[0]; + var node = target.TreeContainerFromItem(item); + Assert.NotNull(node); - var next = KeyboardNavigationHandler.GetNext(node, NavigationDirection.Previous); - } + target.SelectedItem = selectedNode; + node.Focus(); + Assert.Same(node, focus.Current); } [Fact] public void Pressing_SelectAll_Gesture_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var rootNode = tree[0]; - - var keymap = AvaloniaLocator.Current.GetRequiredService(); - var selectAllGesture = keymap.SelectAll.First(); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var keymap = AvaloniaLocator.Current.GetRequiredService(); + var selectAllGesture = keymap.SelectAll.First(); - var keyEvent = new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = selectAllGesture.Key, - KeyModifiers = selectAllGesture.KeyModifiers - }; + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + KeyModifiers = selectAllGesture.KeyModifiers + }; - target.RaiseEvent(keyEvent); + target.RaiseEvent(keyEvent); - AssertChildrenSelected(target, rootNode); - } + AssertChildrenSelected(target, rootNode); } [Fact] public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var rootNode = tree[0]; - - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); - - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(from); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); + var fromContainer = Assert.IsType(target.TreeContainerFromItem(from)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - var keymap = AvaloniaLocator.Current.GetRequiredService(); - var selectAllGesture = keymap.SelectAll.First(); + var keymap = AvaloniaLocator.Current.GetRequiredService(); + var selectAllGesture = keymap.SelectAll.First(); - var keyEvent = new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = selectAllGesture.Key, - KeyModifiers = selectAllGesture.KeyModifiers - }; + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + KeyModifiers = selectAllGesture.KeyModifiers + }; - target.RaiseEvent(keyEvent); + target.RaiseEvent(keyEvent); - AssertChildrenSelected(target, rootNode); - } + AssertChildrenSelected(target, rootNode); } [Fact] public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var rootNode = tree[0]; - - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; - - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(from); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; + var fromContainer = Assert.IsType(target.TreeContainerFromItem(from)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - var keymap = AvaloniaLocator.Current.GetRequiredService(); - var selectAllGesture = keymap.SelectAll.First(); + var keymap = AvaloniaLocator.Current.GetRequiredService(); + var selectAllGesture = keymap.SelectAll.First(); - var keyEvent = new KeyEventArgs - { - RoutedEvent = InputElement.KeyDownEvent, - Key = selectAllGesture.Key, - KeyModifiers = selectAllGesture.KeyModifiers - }; + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + KeyModifiers = selectAllGesture.KeyModifiers + }; - target.RaiseEvent(keyEvent); + target.RaiseEvent(keyEvent); - AssertChildrenSelected(target, rootNode); - } + AssertChildrenSelected(target, rootNode); } [Fact] public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() { - using (UnitTestApplication.Start()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple, - }; - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - var visualRoot = new TestRoot(); - visualRoot.Child = target; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - target.SelectAll(); + target.SelectAll(); - AssertChildrenSelected(target, tree[0]); - Assert.Equal(5, target.SelectedItems.Count); + AssertChildrenSelected(target, data[0]); + Assert.Equal(5, target.SelectedItems.Count); - _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + _mouse.Click(target.Presenter!.Panel!.Children[0], MouseButton.Right); - Assert.Equal(5, target.SelectedItems.Count); - } + Assert.Equal(5, target.SelectedItems.Count); } [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - - var rootNode = tree[0]; - var to = rootNode.Children[0]; - var then = rootNode.Children[1]; - - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(rootNode); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); - var thenContainer = (TreeViewItem)target.TreeContainerFromItem(then); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var to = rootNode.Children[0]; + var then = rootNode.Children[1]; + var fromContainer = Assert.IsType(target.TreeContainerFromItem(rootNode)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); + var thenContainer = Assert.IsType(target.TreeContainerFromItem(then)); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - Assert.Equal(2, target.SelectedItems.Count); + Assert.Equal(2, target.SelectedItems.Count); - _mouse.Click(thenContainer, MouseButton.Right); + _mouse.Click(thenContainer, MouseButton.Right); - Assert.Equal(1, target.SelectedItems.Count); - } + Assert.Equal(1, target.SelectedItems.Count); } [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(from); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = Assert.IsType(target.TreeContainerFromItem(from)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); - Assert.Equal(1, target.SelectedItems.Count); - } + Assert.Equal(1, target.SelectedItems.Count); } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - SelectionMode = SelectionMode.Multiple, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.TreeContainerFromItem(from); - var toContainer = (TreeViewItem)target.TreeContainerFromItem(to); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data, multiSelect: true); + var rootNode = data[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = Assert.IsType(target.TreeContainerFromItem(from)); + var toContainer = Assert.IsType(target.TreeContainerFromItem(to)); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); - Assert.Equal(1, target.SelectedItems.Count); - } + Assert.Equal(1, target.SelectedItems.Count); } [Fact] public void TreeViewItems_Level_Should_Be_Set() { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); Assert.Equal(0, GetItem(target, 0).Level); Assert.Equal(1, GetItem(target, 0, 0).Level); @@ -1624,6 +1087,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void TreeViewItems_Level_Should_Be_Set_For_Derived_TreeView() { + using var app = Start(); var tree = CreateTestTreeData(); var target = new DerivedTreeView { @@ -1631,11 +1095,8 @@ namespace Avalonia.Controls.UnitTests ItemsSource = tree, }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var root = CreateRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); ExpandAll(target); Assert.Equal(0, GetItem(target, 0).Level); @@ -1649,29 +1110,15 @@ namespace Avalonia.Controls.UnitTests public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash() { // Issue #2985 - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); - - var parent = tree[0]; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var parent = data[0]; var node = parent.Children[1]; parent.Children.Remove(node); parent.Children.Add(node); - var item = target.TreeContainerFromItem(node); - ApplyTemplates(new[] { item }); - // #2985 causes ArgumentException here. node.Children.Add(new Node()); } @@ -1680,101 +1127,69 @@ namespace Avalonia.Controls.UnitTests public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { // Issue #2980. - using (Application()) + using var app = Start(); + + var data = new List { - var target = new DerivedTreeView - { - Template = CreateTreeViewTemplate(), - SelectionMode = SelectionMode.Multiple, - ItemsSource = new List - { - new Node { Value = "Root1", }, - new Node { Value = "Root2", }, - }, - }; + new Node { Value = "Root1", }, + new Node { Value = "Root2", }, + }; - var visualRoot = new TestRoot + var style = new Style(x => x.OfType()) + { + Setters = { - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(TreeViewItem.IsExpandedProperty, true), - }, - }, - }, - Child = target, - }; + new Setter(TreeViewItem.IsExpandedProperty, true), + }, + }; - CreateNodeDataTemplate(target); - ApplyTemplates(target); + var target = CreateTarget(data: data, styles: new[] { style }, multiSelect: true); - _mouse.Click(GetItem(target, 0)); - _mouse.Click(GetItem(target, 1), modifiers: KeyModifiers.Shift); - } + _mouse.Click(GetItem(target, 0)); + _mouse.Click(GetItem(target, 1), modifiers: KeyModifiers.Shift); } [Fact] public void Removing_TreeView_From_Root_Should_Preserve_TreeViewItems() { // Issue #3328 - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var root = new TestRoot(); - root.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var root = Assert.IsType(target.GetVisualRoot()); Assert.Equal(5, target.GetRealizedTreeContainers().Count()); root.Child = null; Assert.Equal(5, target.GetRealizedTreeContainers().Count()); - Assert.Equal(1, target.Presenter.Panel.Children.Count); + Assert.Equal(1, target.Presenter!.Panel!.Children.Count); var rootNode = Assert.IsType(target.Presenter.Panel.Children[0]); Assert.Equal(3, rootNode.GetRealizedContainers().Count()); - Assert.Equal(3, rootNode.Presenter.Panel.Children.Count); + Assert.Equal(3, rootNode.Presenter!.Panel!.Children.Count); var child2Node = Assert.IsType(rootNode.Presenter.Panel.Children[1]); Assert.Equal(1, child2Node.GetRealizedContainers().Count()); - Assert.Equal(1, child2Node.Presenter.Panel.Children.Count); + Assert.Equal(1, child2Node.Presenter!.Panel!.Children.Count); } [Fact] public void Clearing_TreeView_Items_Clears_Index() { // Issue #3551 - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var root = new TestRoot(); - root.Child = target; - - CreateNodeDataTemplate(target); - ApplyTemplates(target); - - var rootNode = tree[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(rootNode); + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); + var root = Assert.IsType(target.GetVisualRoot()); + var rootNode = data[0]; + var container = Assert.IsType(target.TreeContainerFromItem(rootNode)); Assert.NotNull(container); root.Child = null; - tree.Clear(); + data.Clear(); Assert.Empty(target.GetRealizedContainers()); } @@ -1789,7 +1204,9 @@ namespace Avalonia.Controls.UnitTests ItemsSource = tree, }; - ApplyTemplates(target); + var root = CreateRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + ExpandAll(target); // Verify that all items are DerivedTreeViewItem foreach (var container in target.GetRealizedTreeContainers()) @@ -1798,39 +1215,54 @@ namespace Avalonia.Controls.UnitTests } } - private void ApplyTemplates(TreeView tree) + private static TreeView CreateTarget(Optional?> data = default, + bool expandAll = true, + ControlTheme? itemContainerTheme = null, + IDataTemplate? itemTemplate = null, + bool multiSelect = false, + IEnumerable + From 904be53ffaa891742d3c51ca5359ea5c41bd53fb Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 29 Mar 2023 08:24:56 +0000 Subject: [PATCH 116/196] merge scroll viewer samples --- samples/ControlCatalog/MainView.xaml | 3 - .../ControlCatalog/Pages/ScrollSnapPage.xaml | 222 -------------- .../Pages/ScrollSnapPage.xaml.cs | 68 ---- .../Pages/ScrollViewerPage.xaml | 290 ++++++++++++++++-- .../Pages/ScrollViewerPage.xaml.cs | 37 +++ 5 files changed, 298 insertions(+), 322 deletions(-) delete mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml delete mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 9f06525821..7ed2d67379 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -147,9 +147,6 @@ - - - diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml deleted file mode 100644 index fa206f0dff..0000000000 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml +++ /dev/null @@ -1,222 +0,0 @@ - - - Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. - - - - - - - - - - - - - - - - Vertical Snapping - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Horizontal Snapping - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs deleted file mode 100644 index 384dc67c66..0000000000 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Markup.Xaml; -using MiniMvvm; - -namespace ControlCatalog.Pages -{ - public class ScrollSnapPageViewModel : ViewModelBase - { - private SnapPointsType _snapPointsType; - private SnapPointsAlignment _snapPointsAlignment; - private bool _areSnapPointsRegular; - - public ScrollSnapPageViewModel() - { - - AvailableSnapPointsType = new List() - { - SnapPointsType.None, - SnapPointsType.Mandatory, - SnapPointsType.MandatorySingle - }; - - AvailableSnapPointsAlignment = new List() - { - SnapPointsAlignment.Near, - SnapPointsAlignment.Center, - SnapPointsAlignment.Far, - }; - } - - public bool AreSnapPointsRegular - { - get => _areSnapPointsRegular; - set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); - } - - public SnapPointsType SnapPointsType - { - get => _snapPointsType; - set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); - } - - public SnapPointsAlignment SnapPointsAlignment - { - get => _snapPointsAlignment; - set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); - } - public List AvailableSnapPointsType { get; } - public List AvailableSnapPointsAlignment { get; } - } - - public class ScrollSnapPage : UserControl - { - public ScrollSnapPage() - { - this.InitializeComponent(); - - DataContext = new ScrollSnapPageViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 1a3d61eb85..4af61c3399 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -3,35 +3,267 @@ xmlns:pages="using:ControlCatalog.Pages" x:Class="ControlCatalog.Pages.ScrollViewerPage" x:DataType="pages:ScrollViewerPageViewModel"> - - Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. - - - - - - - - - - - - - - - + + + + Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. + + + + + + + + + + + + + + + + + + + + + + + + + Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. + + + + + + + - - - - - + + + + + + + + Vertical Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index a097f1f951..7082ca1bf6 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -12,6 +12,9 @@ namespace ControlCatalog.Pages private bool _enableInertia; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; + private SnapPointsType _snapPointsType; + private SnapPointsAlignment _snapPointsAlignment; + private bool _areSnapPointsRegular; public ScrollViewerPageViewModel() { @@ -23,6 +26,20 @@ namespace ControlCatalog.Pages ScrollBarVisibility.Disabled, }; + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; @@ -54,6 +71,26 @@ namespace ControlCatalog.Pages } public List AvailableVisibility { get; } + + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + + public SnapPointsType SnapPointsType + { + get => _snapPointsType; + set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); + } + + public SnapPointsAlignment SnapPointsAlignment + { + get => _snapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); + } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } } public class ScrollViewerPage : UserControl From cd5307aa8c4a4ae08e7e2e808617731180527e3c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 18:36:23 +0200 Subject: [PATCH 117/196] Added failing test. --- .../AvaloniaObjectTests_Inheritance.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs index c5cbda5325..707e1d4470 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs @@ -1,5 +1,10 @@ using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Data; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests @@ -90,6 +95,31 @@ namespace Avalonia.Base.UnitTests Assert.Equal(1, raised); } + [Fact] + public void ClearValue_On_Parent_Raises_PropertyChanged_On_Child_With_Inherited_Grandparent_Value() + { + var grandparent = new Class1(); + var parent = new Class2 { Parent = grandparent }; + var child = new Class2 { Parent = parent }; + var raised = 0; + + grandparent.SetValue(Class1.BazProperty, "grandparent"); + parent.SetValue(Class1.BazProperty, "parent"); + + child.PropertyChanged += (s, e) => + { + Assert.Same(child, e.Sender); + Assert.Equal("parent", e.OldValue); + Assert.Equal("grandparent", e.NewValue); + Assert.Equal(BindingPriority.Inherited, e.Priority); + ++raised; + }; + + parent.ClearValue(Class1.BazProperty); + + Assert.Equal(1, raised); + } + [Fact] public void Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_Has_Value_Set() { From 683afe6e9fdfa265e44289d3f76dc297f117584c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 18:45:48 +0200 Subject: [PATCH 118/196] Use correct newValue on inherited property change. - `oldValue` was misnamed - it's the new value - We calculated the new value and then didn't pass it to children for their property changed events: they were using the default value which may be wrong if the value is set further up the tree --- src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs | 12 ++++++------ src/Avalonia.Base/PropertyStore/ValueStore.cs | 8 +++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 330034f51d..6f0f6702e5 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -140,24 +140,24 @@ namespace Avalonia.PropertyStore var p = (StyledProperty)property; BindingPriority priority; - T oldValue; + T newValue; if (property.Inherits && owner.TryGetInheritedValue(property, out var i)) { - oldValue = ((EffectiveValue)i).Value; + newValue = ((EffectiveValue)i).Value; priority = BindingPriority.Inherited; } else { - oldValue = _metadata.DefaultValue; + newValue = _metadata.DefaultValue; priority = BindingPriority.Unset; } - if (!EqualityComparer.Default.Equals(oldValue, Value)) + if (!EqualityComparer.Default.Equals(newValue, Value)) { - owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true); + owner.Owner.RaisePropertyChanged(p, Value, newValue, priority, true); if (property.Inherits) - owner.OnInheritedEffectiveValueDisposed(p, Value); + owner.OnInheritedEffectiveValueDisposed(p, Value, newValue); } if (ValueEntry?.GetDataValidationState(out _, out _) ?? diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index af31459a98..2b5a2f5022 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -419,7 +419,9 @@ namespace Avalonia.PropertyStore ReevaluateEffectiveValue(property, current); } else + { ReevaluateEffectiveValues(); + } } /// @@ -481,7 +483,8 @@ namespace Avalonia.PropertyStore /// /// The property whose value changed. /// The old value of the property. - public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue) + /// The new value of the property. + public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue, T newValue) { Debug.Assert(property.Inherits); @@ -489,12 +492,11 @@ namespace Avalonia.PropertyStore if (children is not null) { - var defaultValue = property.GetDefaultValue(Owner.GetType()); var count = children.Count; for (var i = 0; i < count; ++i) { - children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue); + children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue); } } } From fd5a3168b737c30067b3c9692fbcb1e2dbf0c143 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 21:58:41 +0200 Subject: [PATCH 119/196] Update simple theme caption buttons. --- .../Controls/CaptionButtons.xaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml index 4522ffac87..040de9ddbd 100644 --- a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml @@ -54,7 +54,8 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/VirtualizationDemo/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs deleted file mode 100644 index cea200dcec..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using VirtualizationDemo.ViewModels; - -namespace VirtualizationDemo -{ - public class MainWindow : Window - { - public MainWindow() - { - this.InitializeComponent(); - this.AttachDevTools(); - DataContext = new MainWindowViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/VirtualizationDemo/Models/Chat.cs b/samples/VirtualizationDemo/Models/Chat.cs new file mode 100644 index 0000000000..e84760135c --- /dev/null +++ b/samples/VirtualizationDemo/Models/Chat.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace VirtualizationDemo.Models; + +public class ChatFile +{ + public ChatMessage[] Chat { get; set; } + + public static ChatFile Load(string path) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + using var s = File.OpenRead(path); + return JsonSerializer.Deserialize(s, options); + } +} + +public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index febda46450..87212b6daa 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -1,15 +1,14 @@ using Avalonia; -namespace VirtualizationDemo +namespace VirtualizationDemo; + +class Program { - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } diff --git a/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs new file mode 100644 index 0000000000..5ade0ec9ec --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using System.IO; +using VirtualizationDemo.Models; + +namespace VirtualizationDemo.ViewModels; + +public class ChatPageViewModel +{ + public ChatPageViewModel() + { + var chat = ChatFile.Load(Path.Combine("Assets", "chat.json")); + Messages = new(chat.Chat); + } + + public ObservableCollection Messages { get; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs deleted file mode 100644 index 9ba505ffe5..0000000000 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using MiniMvvm; - -namespace VirtualizationDemo.ViewModels -{ - internal class ItemViewModel : ViewModelBase - { - private string _prefix; - private int _index; - private double _height = double.NaN; - - public ItemViewModel(int index, string prefix = "Item") - { - _prefix = prefix; - _index = index; - } - - public string Header => $"{_prefix} {_index}"; - - public double Height - { - get => _height; - set => this.RaiseAndSetIfChanged(ref _height, value); - } - } -} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 96dbbc1a83..6d3590307c 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -1,160 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Layout; -using Avalonia.Controls.Selection; -using MiniMvvm; +using MiniMvvm; -namespace VirtualizationDemo.ViewModels -{ - internal class MainWindowViewModel : ViewModelBase - { - private int _itemCount = 200; - private string _newItemString = "New Item"; - private int _newItemIndex; - private AvaloniaList _items; - private string _prefix = "Item"; - private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; - private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; - private Orientation _orientation = Orientation.Vertical; - - public MainWindowViewModel() - { - this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); - RecreateCommand = MiniCommand.Create(() => Recreate()); - - AddItemCommand = MiniCommand.Create(() => AddItem()); - - RemoveItemCommand = MiniCommand.Create(() => Remove()); - - SelectFirstCommand = MiniCommand.Create(() => SelectItem(0)); - - SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1)); - } - - public string NewItemString - { - get { return _newItemString; } - set { this.RaiseAndSetIfChanged(ref _newItemString, value); } - } - - public int ItemCount - { - get { return _itemCount; } - set { this.RaiseAndSetIfChanged(ref _itemCount, value); } - } - - public SelectionModel Selection { get; } = new SelectionModel(); - - public AvaloniaList Items - { - get { return _items; } - private set { this.RaiseAndSetIfChanged(ref _items, value); } - } - - public Orientation Orientation - { - get { return _orientation; } - set { this.RaiseAndSetIfChanged(ref _orientation, value); } - } - - public IEnumerable Orientations => - Enum.GetValues(typeof(Orientation)).Cast(); - - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get { return _horizontalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); } - } +namespace VirtualizationDemo.ViewModels; - public ScrollBarVisibility VerticalScrollBarVisibility - { - get { return _verticalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); } - } - - public IEnumerable ScrollBarVisibilities => - Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - - public MiniCommand AddItemCommand { get; private set; } - public MiniCommand RecreateCommand { get; private set; } - public MiniCommand RemoveItemCommand { get; private set; } - public MiniCommand SelectFirstCommand { get; private set; } - public MiniCommand SelectLastCommand { get; private set; } - - public void RandomizeSize() - { - var random = new Random(); - - foreach (var i in Items) - { - i.Height = random.Next(240) + 10; - } - } - - public void ResetSize() - { - foreach (var i in Items) - { - i.Height = double.NaN; - } - } - - private void ResizeItems(int count) - { - if (Items == null) - { - var items = Enumerable.Range(0, count) - .Select(x => new ItemViewModel(x)); - Items = new AvaloniaList(items); - } - else if (count > Items.Count) - { - var items = Enumerable.Range(Items.Count, count - Items.Count) - .Select(x => new ItemViewModel(x)); - Items.AddRange(items); - } - else if (count < Items.Count) - { - Items.RemoveRange(count, Items.Count - count); - } - } - - private void AddItem() - { - var index = Items.Count; - - if (Selection.SelectedItems.Count > 0) - { - index = Selection.SelectedIndex; - } - - Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); - } - - private void Remove() - { - if (Selection.SelectedItems.Count > 0) - { - Items.RemoveAll(Selection.SelectedItems.ToList()); - } - } - - private void Recreate() - { - _prefix = _prefix == "Item" ? "Recreated" : "Item"; - var items = Enumerable.Range(0, _itemCount) - .Select(x => new ItemViewModel(x, _prefix)); - Items = new AvaloniaList(items); - } - - private void SelectItem(int index) - { - Selection.SelectedIndex = index; - } - } +internal class MainWindowViewModel : ViewModelBase +{ + public ChatPageViewModel Chat { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml b/samples/VirtualizationDemo/Views/ChatPageView.axaml new file mode 100644 index 0000000000..fc182f15ae --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs new file mode 100644 index 0000000000..b5c90db69c --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace VirtualizationDemo.Views; + +public partial class ChatPageView : UserControl +{ + public ChatPageView() + { + InitializeComponent(); + } +} diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 81b30c6cbe..3ac7aab589 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,19 +1,24 @@  - Exe + WinExe net6.0 + true + + + - - + + + + + PreserveNewest + - - - - - + + From 35d70577f48d352c9dec7ce3e9389ff2ac4bc7d4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:45:45 +0200 Subject: [PATCH 137/196] Added repro for #10367 to virtualization demo. --- samples/VirtualizationDemo/MainWindow.axaml | 3 +++ .../ViewModels/ExpanderItemViewModel.cs | 21 +++++++++++++++++++ .../ViewModels/ExpanderPageViewModel.cs | 17 +++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 1 + .../Views/ExpanderPageView.axaml | 18 ++++++++++++++++ .../Views/ExpanderPageView.axaml.cs | 13 ++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs diff --git a/samples/VirtualizationDemo/MainWindow.axaml b/samples/VirtualizationDemo/MainWindow.axaml index 94e7c96a76..e064e6ab32 100644 --- a/samples/VirtualizationDemo/MainWindow.axaml +++ b/samples/VirtualizationDemo/MainWindow.axaml @@ -12,5 +12,8 @@ + + + diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs new file mode 100644 index 0000000000..a17fc2d303 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs @@ -0,0 +1,21 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class ExpanderItemViewModel : ViewModelBase +{ + private string? _header; + private bool _isExpanded; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 0000000000..f2807a803b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using System.Linq; + +namespace VirtualizationDemo.ViewModels; + +internal class ExpanderPageViewModel +{ + public ExpanderPageViewModel() + { + Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel + { + Header = $"Item {x}", + })); + } + + public ObservableCollection Items { get; set; } +} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 6d3590307c..6432503595 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -5,4 +5,5 @@ namespace VirtualizationDemo.ViewModels; internal class MainWindowViewModel : ViewModelBase { public ChatPageViewModel Chat { get; } = new(); + public ExpanderPageViewModel Expanders { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml new file mode 100644 index 0000000000..972d885229 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs new file mode 100644 index 0000000000..df3689cf24 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo.Views; + +public partial class ExpanderPageView : UserControl +{ + public ExpanderPageView() + { + InitializeComponent(); + } +} \ No newline at end of file From 345fb7e1d6b8aaadf01b3f7f2f722dbff433a261 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 16:23:57 +0200 Subject: [PATCH 138/196] Register anchor candidate in panel. We need to register controls as anchor candidates in the panel instead of in `ItemsControl` because the candidate needs to be registered after arrange. Consider this scenario: - In Measure: - Container is realized and registered as an anchor candidate - Container is unrealized and unregistered - Container is recycled and registered, but it is still placed in the position from before it was recycled - In Arrange: - The container is placed in its new position - The `ScrollContentPresenter` sees it's been moved and adjusts the viewport to anchor it This is obviously incorrect, but was what was happening when `ItemsControl` was responsible for registering anchor candidates. Instead of tracking which containers have already been registered, change the list of anchor candidates in `ScrollContentPresenter` to a `HashSet` so we can just register it multiple times. --- src/Avalonia.Controls/ItemsControl.cs | 4 ---- .../Presenters/ScrollContentPresenter.cs | 5 +++-- src/Avalonia.Controls/VirtualizingStackPanel.cs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 54ffba462f..4d71cc8d4f 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -94,7 +94,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; /// @@ -457,7 +456,6 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } @@ -629,7 +627,6 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); - _scrollViewer?.RegisterAnchorCandidate(container); ContainerPrepared?.Invoke(this, new(container, index)); } @@ -642,7 +639,6 @@ namespace Avalonia.Controls internal void ClearItemContainer(Control container) { - _scrollViewer?.UnregisterAnchorCandidate(container); ClearContainerForItemOverride(container); ContainerClearing?.Invoke(this, new(container)); } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bc86558ab3..261b7d3533 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Presenters private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; private Dictionary? _scrollGestureSnapPoints; - private List? _anchorCandidates; + private HashSet? _anchorCandidates; private Control? _anchorElement; private Rect _anchorElementBounds; private bool _isAnchorElementDirty; @@ -310,7 +310,7 @@ namespace Avalonia.Controls.Presenters "An anchor control must be a visual descendent of the ScrollContentPresenter."); } - _anchorCandidates ??= new List(); + _anchorCandidates ??= new(); _anchorCandidates.Add(element); _isAnchorElementDirty = true; } @@ -410,6 +410,7 @@ namespace Avalonia.Controls.Presenters try { _arranging = true; + Offset = newOffset; } finally diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 5a766b9cd3..ad2ae9278c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -67,6 +67,7 @@ namespace Avalonia.Controls private double _lastEstimatedElementSizeU = 25; private RealizedStackElements? _measureElements; private RealizedStackElements? _realizedElements; + private ScrollViewer? _scrollViewer; private Rect _viewport = s_invalidViewport; private Stack? _recyclePool; private Control? _unrealizedFocusedElement; @@ -203,6 +204,7 @@ namespace Avalonia.Controls new Rect(u, 0, sizeU, finalSize.Height) : new Rect(0, u, finalSize.Width, sizeU); e.Arrange(rect); + _scrollViewer?.RegisterAnchorCandidate(e); u += orientation == Orientation.Horizontal ? rect.Width : rect.Height; } } @@ -217,6 +219,18 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _scrollViewer = this.FindAncestorOfType(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _scrollViewer = null; + } + protected override void OnItemsChanged(IReadOnlyList items, NotifyCollectionChangedEventArgs e) { InvalidateMeasure(); @@ -598,6 +612,8 @@ namespace Avalonia.Controls { Debug.Assert(ItemContainerGenerator is not null); + _scrollViewer?.UnregisterAnchorCandidate(element); + if (element.IsSet(ItemIsOwnContainerProperty)) { element.IsVisible = false; From 54449850cc39d9c04c13412fc3fb9f27c35c03aa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 22:56:13 +0200 Subject: [PATCH 139/196] Make thumb drag delta relative to parent. Prevents the viewport jumping around when scrolling a virtualized list with differing sizes. Instead the thumb jumps around. --- src/Avalonia.Controls/Primitives/Thumb.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index cb3195cf52..c205830bc2 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -80,20 +81,22 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { + var point = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, - Vector = e.GetPosition(this) - _lastPoint.Value, + Vector = point - _lastPoint.Value, }; RaiseEvent(ev); + _lastPoint = point; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this); + _lastPoint = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { @@ -116,7 +119,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this), + Vector = (Vector)e.GetPosition(this.GetVisualParent()), }; RaiseEvent(ev); From bc32c061e8fe3229cd60ff4a1767e83389dd0667 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:06:50 +0200 Subject: [PATCH 140/196] Added tests to detect scroll jumps. --- .../VirtualizingStackPanel.cs | 10 ++ .../VirtualizingStackPanelTests.cs | 146 +++++++++++++++++- 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index ad2ae9278c..7cca0986ad 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -130,6 +130,16 @@ namespace Avalonia.Controls set { SetValue(AreVerticalSnapPointsRegularProperty, value); } } + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1; + protected override Size MeasureOverride(Size availableSize) { if (!IsEffectivelyVisible) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 76fc31c50d..2cdd4eaf95 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10}); - var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false); + var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null); Assert.Equal(1000, scroll.Extent.Height); @@ -252,7 +252,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = new ObservableCollection