From 7520967a110ee30655c1bf2a7eaef29ffd3300c4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 Mar 2026 13:50:53 +0500 Subject: [PATCH 01/24] Streamline drawn decorations management to avoid call ordering problems (#20840) --- .../TopLevelHost.Decorations.cs | 104 ++++++++++-------- src/Avalonia.Controls/Window.cs | 24 ++-- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/src/Avalonia.Controls/TopLevelHost.Decorations.cs b/src/Avalonia.Controls/TopLevelHost.Decorations.cs index f9cb20f511..11f3dc3ada 100644 --- a/src/Avalonia.Controls/TopLevelHost.Decorations.cs +++ b/src/Avalonia.Controls/TopLevelHost.Decorations.cs @@ -48,64 +48,76 @@ internal partial class TopLevelHost internal WindowDrawnDecorations? Decorations => _decorations; /// - /// Enables drawn window decorations with the specified parts. - /// Creates the decorations instance, applies the template, and inserts layers into the visual tree. + /// Updates drawn window decorations with the specified parts and window state. + /// When is null, decorations are removed entirely. + /// When non-null (including ), the decoration + /// infrastructure is kept alive and parts/fullscreen state are updated. /// - internal void EnableDecorations(DrawnWindowDecorationParts parts) + internal void UpdateDrawnDecorations(DrawnWindowDecorationParts? parts, WindowState windowState) { + if (parts == null) + { + RemoveDecorations(); + return; + } + + var enabledParts = parts.Value; + if (_decorations != null) { // Layers persist across part changes; pseudo-classes driven by EnabledParts // control visibility of individual decoration elements in the theme. - _decorations.EnabledParts = parts; + _decorations.EnabledParts = enabledParts; if (_resizeGrips != null) - _resizeGrips.IsVisible = parts.HasFlag(DrawnWindowDecorationParts.ResizeGrips); - return; + _resizeGrips.IsVisible = enabledParts.HasFlag(DrawnWindowDecorationParts.ResizeGrips); } - - _decorations = new WindowDrawnDecorations(); - _decorations.EnabledParts = parts; - - // Set up logical parenting - LogicalChildren.Add(_decorations); - - // Create layer wrappers - _underlay = new LayerWrapper() { [AutomationProperties.AutomationIdProperty] = "WindowChromeUnderlay" }; - _overlay = new LayerWrapper() { [AutomationProperties.AutomationIdProperty] = "WindowChromeOverlay" }; - _fullscreenPopover = new LayerWrapper() + else { - IsVisible = false, [AutomationProperties.AutomationIdProperty] = "PopoverWindowChrome" - }; - - // Insert layers: underlay below TopLevel, overlay and popover above - // Visual order: underlay(0), TopLevel(1), overlay(2), fullscreenPopover(3), resizeGrips(4) - VisualChildren.Insert(0, _underlay); - VisualChildren.Add(_overlay); - VisualChildren.Add(_fullscreenPopover); - - // Always create resize grips; visibility is controlled by EnabledParts - _resizeGrips = new ResizeGripLayer(); - _resizeGrips.IsVisible = parts.HasFlag(DrawnWindowDecorationParts.ResizeGrips); - VisualChildren.Add(_resizeGrips); + _decorations = new WindowDrawnDecorations(); + _decorations.EnabledParts = enabledParts; - // Attach to window if available - if (_topLevel is Window window) - _decorations.Attach(window); + // Set up logical parenting + LogicalChildren.Add(_decorations); - // Subscribe to template changes to re-apply and geometry changes for resize grips - _decorations.EffectiveGeometryChanged += OnDecorationsGeometryChanged; - _decorationsSubscriptions = _decorations.GetObservable(WindowDrawnDecorations.TemplateProperty) - .Subscribe(_ => ApplyDecorationsTemplate()); + // Create layer wrappers + _underlay = new LayerWrapper() { [AutomationProperties.AutomationIdProperty] = "WindowChromeUnderlay" }; + _overlay = new LayerWrapper() { [AutomationProperties.AutomationIdProperty] = "WindowChromeOverlay" }; + _fullscreenPopover = new LayerWrapper() + { + IsVisible = false, [AutomationProperties.AutomationIdProperty] = "PopoverWindowChrome" + }; + + // Insert layers: underlay below TopLevel, overlay and popover above + // Visual order: underlay(0), TopLevel(1), overlay(2), fullscreenPopover(3), resizeGrips(4) + VisualChildren.Insert(0, _underlay); + VisualChildren.Add(_overlay); + VisualChildren.Add(_fullscreenPopover); + + // Always create resize grips; visibility is controlled by EnabledParts + _resizeGrips = new ResizeGripLayer(); + _resizeGrips.IsVisible = enabledParts.HasFlag(DrawnWindowDecorationParts.ResizeGrips); + VisualChildren.Add(_resizeGrips); + + // Attach to window if available + if (_topLevel is Window window) + _decorations.Attach(window); + + // Subscribe to template changes to re-apply and geometry changes for resize grips + _decorations.EffectiveGeometryChanged += OnDecorationsGeometryChanged; + _decorationsSubscriptions = _decorations.GetObservable(WindowDrawnDecorations.TemplateProperty) + .Subscribe(_ => ApplyDecorationsTemplate()); + + ApplyDecorationsTemplate(); + InvalidateMeasure(); + } - ApplyDecorationsTemplate(); - InvalidateMeasure(); - _decorationsOverlayPeer?.InvalidateChildren(); + ApplyFullscreenState(windowState == WindowState.FullScreen); } /// - /// Disables drawn window decorations and removes all layers. + /// Removes drawn window decorations and all associated layers. /// - internal void DisableDecorations() + private void RemoveDecorations() { if (_decorations == null) return; @@ -192,15 +204,15 @@ internal partial class TopLevelHost } /// - /// Shows or hides the fullscreen popover based on the window state. - /// Called by Window when window state changes. + /// Applies fullscreen-specific layer visibility: hides overlay/underlay and enables + /// popover hover detection, or restores normal state. /// - internal void SetFullscreenPopoverEnabled(bool enabled) + private void ApplyFullscreenState(bool isFullscreen) { if (_fullscreenPopover == null) return; - if (enabled) + if (isFullscreen) { // In fullscreen mode, hide overlay and underlay, enable popover hover detection if (_overlay != null) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 043abb0e1b..d92a46a70a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -626,10 +626,7 @@ namespace Avalonia.Controls StartRendering(); } - // Update fullscreen popover visibility - TopLevelHost.SetFullscreenPopoverEnabled(state == WindowState.FullScreen); - - // Update decoration parts for the new window state + // Update decoration parts and fullscreen popover state for the new window state UpdateDrawnDecorationParts(); } @@ -643,13 +640,11 @@ namespace Avalonia.Controls private void UpdateDrawnDecorations() { - var needsDrawnDecorations = PlatformImpl?.NeedsManagedDecorations ?? false; + var parts = ComputeDecorationParts(); + TopLevelHost.UpdateDrawnDecorations(parts, WindowState); - var parts = needsDrawnDecorations ? ComputeDecorationParts() : DrawnWindowDecorationParts.None; - if (parts != DrawnWindowDecorationParts.None) + if (parts != null) { - TopLevelHost.EnableDecorations(parts); - // Forward ExtendClientAreaTitleBarHeightHint to decoration TitleBarHeight var decorations = TopLevelHost.Decorations; if (decorations != null) @@ -659,10 +654,6 @@ namespace Avalonia.Controls decorations.TitleBarHeightOverride = hint; } } - else - { - TopLevelHost.DisableDecorations(); - } UpdateDrawnDecorationMargins(); } @@ -676,11 +667,14 @@ namespace Avalonia.Controls if (TopLevelHost.Decorations == null) return; - TopLevelHost.EnableDecorations(ComputeDecorationParts()); + TopLevelHost.UpdateDrawnDecorations(ComputeDecorationParts(), WindowState); } - private Chrome.DrawnWindowDecorationParts ComputeDecorationParts() + private Chrome.DrawnWindowDecorationParts? ComputeDecorationParts() { + if (!(PlatformImpl?.NeedsManagedDecorations ?? false)) + return null; + var platformNeeds = PlatformImpl?.RequestedDrawnDecorations ?? PlatformRequestedDrawnDecoration.None; var parts = Chrome.DrawnWindowDecorationParts.None; if (WindowDecorations != WindowDecorations.None) From f3df9b1f30e8ed15a3b3fc5157f0b8c183297219 Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Mon, 9 Mar 2026 18:41:53 +0900 Subject: [PATCH 02/24] [Metal] Dispose GRBackendRenderTarget and @autoreleasepool for metal objects (#20815) * Add @autoreleasepool to release memory * Pass in and dispose GRBackendRenderTarget * Use Macros --- native/Avalonia.Native/src/OSX/metal.mm | 15 +++++++++------ src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs | 13 +++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/metal.mm b/native/Avalonia.Native/src/OSX/metal.mm index 33aa2aeb53..517872b147 100644 --- a/native/Avalonia.Native/src/OSX/metal.mm +++ b/native/Avalonia.Native/src/OSX/metal.mm @@ -87,11 +87,12 @@ public: return (__bridge void*) queue; } - HRESULT ImportIOSurface(void *handle, AvnPixelFormat pixelFormat, IAvnMetalTexture **ppv) override { + HRESULT ImportIOSurface(void *handle, AvnPixelFormat pixelFormat, IAvnMetalTexture **ppv) override { + START_COM_ARP_CALL; auto surf = (IOSurfaceRef)handle; auto width = IOSurfaceGetWidth(surf); auto height = IOSurfaceGetHeight(surf); - + auto desc = [MTLTextureDescriptor new]; if(pixelFormat == kAvnRgba8888) desc.pixelFormat = MTLPixelFormatRGBA8Unorm; @@ -106,13 +107,12 @@ public: desc.mipmapLevelCount = 1; desc.sampleCount = 1; desc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget; - + auto texture = [device newTextureWithDescriptor:desc iosurface:surf plane:0]; if(texture == nullptr) return E_FAIL; *ppv = new AvnMetalTexture(texture); return S_OK; - } HRESULT ImportSharedEvent(void *mtlSharedEventInstance, IAvnMTLSharedEvent**ppv) override { @@ -132,11 +132,12 @@ public: HRESULT SignalOrWait(IAvnMTLSharedEvent *ev, uint64_t value, bool wait) { + START_ARP_CALL; if (@available(macOS 12.0, *)) { auto e = dynamic_cast(ev); if(e == nullptr) - return E_FAIL;; + return E_FAIL; auto buf = [queue commandBuffer]; if(wait) [buf encodeWaitForEvent:e->GetEvent() value:value]; @@ -204,6 +205,7 @@ public: ~AvnMetalRenderSession() { + START_ARP_CALL; auto buffer = [_queue commandBuffer]; [buffer presentDrawable: _drawable]; [buffer commit]; @@ -227,6 +229,7 @@ public: } HRESULT BeginDrawing(IAvnMetalRenderingSession **ret) override { + START_COM_ARP_CALL; if([NSThread isMainThread]) { // Flush all existing rendering @@ -289,7 +292,7 @@ class AvnMetalDisplay : public ComSingleObject false; @@ -118,14 +118,17 @@ internal class SkiaMetalGpu : ISkiaGpu private readonly SkiaMetalGpu _gpu; private SKSurface? _surface; private IMetalPlatformSurfaceRenderingSession? _session; + private GRBackendRenderTarget? _backendTarget; - public SkiaMetalRenderSession(SkiaMetalGpu gpu, + public SkiaMetalRenderSession(SkiaMetalGpu gpu, SKSurface surface, - IMetalPlatformSurfaceRenderingSession session) + IMetalPlatformSurfaceRenderingSession session, + GRBackendRenderTarget backendTarget) { _gpu = gpu; _surface = surface; _session = session; + _backendTarget = backendTarget; } public void Dispose() @@ -133,11 +136,13 @@ internal class SkiaMetalGpu : ISkiaGpu _surface?.Canvas.Flush(); _surface?.Flush(); _gpu._context?.Flush(); - + _surface?.Dispose(); _surface = null; _session?.Dispose(); _session = null; + _backendTarget?.Dispose(); + _backendTarget = null; } public GRContext GrContext => _gpu._context!; From d8a0008ef03292ee60407b974cb63727165d7c41 Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Mon, 9 Mar 2026 21:00:37 +0900 Subject: [PATCH 03/24] [Navigation] Fire Page lifecycle events after transitions complete, not before (#20826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Send Page lifecycle events after page transitions * Cleanup * Moved the push lifecycle calls out of ExecutePushCore --------- Co-authored-by: Javier Suárez Ruiz --- src/Avalonia.Controls/Page/NavigationPage.cs | 86 +++++++-- .../NavigationPageTests.cs | 164 ++++++++++++++++++ 2 files changed, 231 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Page/NavigationPage.cs b/src/Avalonia.Controls/Page/NavigationPage.cs index 2fb5235ff7..dd14d71a04 100644 --- a/src/Avalonia.Controls/Page/NavigationPage.cs +++ b/src/Avalonia.Controls/Page/NavigationPage.cs @@ -43,6 +43,7 @@ namespace Avalonia.Controls private ContentPresenter? _pagePresenter; private ContentPresenter? _pageBackPresenter; private CancellationTokenSource? _currentTransition; + private Task _lastPageTransitionTask = Task.CompletedTask; private CancellationTokenSource? _currentModalTransition; private Border? _navBar; private Border? _navBarShadow; @@ -769,15 +770,13 @@ namespace Avalonia.Controls page.SetInNavigationPage(true); UpdateActivePage(); - - previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Push)); - page.SendNavigatedTo(new NavigatedToEventArgs(previousPage, NavigationType.Push)); - Pushed?.Invoke(this, new NavigationEventArgs(page, NavigationType.Push)); } /// - /// Performs the stack mutation and lifecycle events for a pop. The visual transition runs - /// subsequently via . + /// Performs the stack mutation for a pop. The visual transition runs + /// subsequently via . Callers are responsible + /// for firing lifecycle events via + /// after awaiting the page transition where possible. /// private Page? ExecutePopCore() { @@ -810,11 +809,6 @@ namespace Avalonia.Controls { old.Navigation = null; old.SetInNavigationPage(false); - - var newCurrentPage = CurrentPage; - old.SendNavigatedFrom(new NavigatedFromEventArgs(newCurrentPage, NavigationType.Pop)); - newCurrentPage?.SendNavigatedTo(new NavigatedToEventArgs(old, NavigationType.Pop)); - Popped?.Invoke(this, new NavigationEventArgs(old, NavigationType.Pop)); } return old; @@ -844,6 +838,12 @@ namespace Avalonia.Controls } ExecutePushCore(page, previousPage); + + await AwaitPageTransitionAsync(); + + previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Push)); + page.SendNavigatedTo(new NavigatedToEventArgs(previousPage, NavigationType.Push)); + Pushed?.Invoke(this, new NavigationEventArgs(page, NavigationType.Push)); } finally { @@ -886,7 +886,14 @@ namespace Avalonia.Controls return null; } - return ExecutePopCore(); + var old = ExecutePopCore(); + + await AwaitPageTransitionAsync(); + + if (old != null) + SendPopLifecycleEvents(old, NavigationType.Pop); + + return old; } finally { @@ -931,6 +938,7 @@ namespace Avalonia.Controls } bool isIncc = Pages is INotifyCollectionChanged; + var poppedPages = new List(); void TearDownPopped(Page popped) { @@ -939,8 +947,7 @@ namespace Avalonia.Controls LogicalChildren.Remove(poppedLogical); popped.Navigation = null; popped.SetInNavigationPage(false); - popped.SendNavigatedFrom(new NavigatedFromEventArgs(rootPage, NavigationType.PopToRoot)); - Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.PopToRoot)); + poppedPages.Add(popped); } if (Pages is Stack stack) @@ -962,6 +969,14 @@ namespace Avalonia.Controls _isPop = true; UpdateActivePage(); + await AwaitPageTransitionAsync(); + + foreach (var popped in poppedPages) + { + popped.SendNavigatedFrom(new NavigatedFromEventArgs(rootPage, NavigationType.PopToRoot)); + Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.PopToRoot)); + } + var newCurrentPage = CurrentPage; if (newCurrentPage != null) @@ -1013,6 +1028,7 @@ namespace Avalonia.Controls } bool isIncc = Pages is INotifyCollectionChanged; + var poppedPages = new List(); void TearDownPopped(Page popped) { @@ -1021,8 +1037,7 @@ namespace Avalonia.Controls LogicalChildren.Remove(poppedLogical); popped.Navigation = null; popped.SetInNavigationPage(false); - popped.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Pop)); - Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.Pop)); + poppedPages.Add(popped); } if (Pages is Stack stack) @@ -1044,6 +1059,14 @@ namespace Avalonia.Controls _isPop = true; UpdateActivePage(); + await AwaitPageTransitionAsync(); + + foreach (var popped in poppedPages) + { + popped.SendNavigatedFrom(new NavigatedFromEventArgs(page, NavigationType.Pop)); + Popped?.Invoke(this, new NavigationEventArgs(popped, NavigationType.Pop)); + } + var newCurrentPage = CurrentPage; if (newCurrentPage != null) { @@ -1356,7 +1379,9 @@ namespace Avalonia.Controls { if (stack.Count > 0 && ReferenceEquals(stack.Peek(), page)) { - ExecutePopCore(); + var old = ExecutePopCore(); + if (old != null) + SendPopLifecycleEvents(old, NavigationType.Pop); PageRemoved?.Invoke(this, new PageRemovedEventArgs(page)); return; } @@ -1387,7 +1412,9 @@ namespace Avalonia.Controls if (idx == list.Count - 1) { - ExecutePopCore(); + var old = ExecutePopCore(); + if (old != null) + SendPopLifecycleEvents(old, NavigationType.Pop); PageRemoved?.Invoke(this, new PageRemovedEventArgs(page)); return; } @@ -1595,12 +1622,14 @@ namespace Avalonia.Controls oldPresenter.ZIndex = 0; } - _ = RunPageTransitionAsync(resolvedTransition, oldPresenter, newPresenter, !isPop, cancel.Token); + _lastPageTransitionTask = RunPageTransitionAsync(resolvedTransition, oldPresenter, newPresenter, !isPop, cancel.Token); (_pagePresenter, _pageBackPresenter) = (newPresenter, oldPresenter); } else { + _lastPageTransitionTask = Task.CompletedTask; + _pagePresenter.Content = page; _pagePresenter.IsVisible = page != null; _pagePresenter.ZIndex = 0; @@ -1686,6 +1715,25 @@ namespace Avalonia.Controls from.Opacity = 1; } + private Task AwaitPageTransitionAsync() + { + var task = _lastPageTransitionTask; + _lastPageTransitionTask = Task.CompletedTask; + return task; + } + + /// + /// Fires lifecycle events after a pop: SendNavigatedFrom on the old page, + /// SendNavigatedTo on the new current page, and raises the Popped event. + /// + private void SendPopLifecycleEvents(Page oldPage, NavigationType navigationType) + { + var newCurrentPage = CurrentPage; + oldPage.SendNavigatedFrom(new NavigatedFromEventArgs(newCurrentPage, navigationType)); + newCurrentPage?.SendNavigatedTo(new NavigatedToEventArgs(oldPage, navigationType)); + Popped?.Invoke(this, new NavigationEventArgs(oldPage, navigationType)); + } + /// /// Swaps the top of the navigation stack with . /// diff --git a/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs b/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs index 8f3055a2fd..9602256fe8 100644 --- a/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/NavigationPageTests.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.UnitTests; @@ -1574,4 +1578,164 @@ public class NavigationPageTests } } + public class LifecycleAfterTransitionTests : ScopedTestBase + { + [Fact] + public async Task PushAsync_LifecycleEvents_FireAfterTransition() + { + var tcs = new TaskCompletionSource(); + var transition = new ControllableTransition(tcs.Task); + var nav = CreateNavigationPage(transition); + + var root = new ContentPage { Header = "Root" }; + await nav.PushAsync(root); + + bool navigatedFromDuringTransition = false; + bool navigatedToDuringTransition = false; + bool pushedDuringTransition = false; + + var second = new ContentPage { Header = "Second" }; + root.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + second.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted; + nav.Pushed += (_, _) => pushedDuringTransition = !tcs.Task.IsCompleted; + + var pushTask = nav.PushAsync(second); + + tcs.SetResult(); + await pushTask; + + Assert.False(navigatedFromDuringTransition); + Assert.False(navigatedToDuringTransition); + Assert.False(pushedDuringTransition); + } + + [Fact] + public async Task PopAsync_LifecycleEvents_FireAfterTransition() + { + var tcs = new TaskCompletionSource(); + var nav = CreateNavigationPage(null); + + var root = new ContentPage { Header = "Root" }; + var top = new ContentPage { Header = "Top" }; + await nav.PushAsync(root); + await nav.PushAsync(top); + + nav.PageTransition = new ControllableTransition(tcs.Task); + + bool navigatedFromDuringTransition = false; + bool navigatedToDuringTransition = false; + bool poppedDuringTransition = false; + + top.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + root.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted; + nav.Popped += (_, _) => poppedDuringTransition = !tcs.Task.IsCompleted; + + var popTask = nav.PopAsync(); + + tcs.SetResult(); + await popTask; + + Assert.False(navigatedFromDuringTransition); + Assert.False(navigatedToDuringTransition); + Assert.False(poppedDuringTransition); + } + + [Fact] + public async Task PopToRootAsync_LifecycleEvents_FireAfterTransition() + { + var tcs = new TaskCompletionSource(); + var nav = CreateNavigationPage(null); + + var root = new ContentPage { Header = "Root" }; + var second = new ContentPage { Header = "Second" }; + var third = new ContentPage { Header = "Third" }; + await nav.PushAsync(root); + await nav.PushAsync(second); + await nav.PushAsync(third); + + nav.PageTransition = new ControllableTransition(tcs.Task); + + bool navigatedFromDuringTransition = false; + bool navigatedToDuringTransition = false; + bool poppedToRootDuringTransition = false; + + second.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + third.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted; + root.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted; + nav.PoppedToRoot += (_, _) => poppedToRootDuringTransition = !tcs.Task.IsCompleted; + + var popTask = nav.PopToRootAsync(); + + tcs.SetResult(); + await popTask; + + Assert.False(navigatedFromDuringTransition); + Assert.False(navigatedToDuringTransition); + Assert.False(poppedToRootDuringTransition); + } + + private static NavigationPage CreateNavigationPage(IPageTransition? transition) + { + var nav = new NavigationPage + { + PageTransition = transition, + Template = NavigationPageTemplate() + }; + var root = new TestRoot { Child = nav }; + root.LayoutManager.ExecuteInitialLayoutPass(); + return nav; + } + + private static IControlTemplate NavigationPageTemplate() + { + return new FuncControlTemplate((parent, ns) => + { + var contentHost = new Panel + { + Name = "PART_ContentHost", + Children = + { + new ContentPresenter { Name = "PART_PageBackPresenter" }.RegisterInNameScope(ns), + new ContentPresenter { Name = "PART_PagePresenter" }.RegisterInNameScope(ns), + } + }.RegisterInNameScope(ns); + + return new Panel + { + Children = + { + new Border + { + Name = "PART_NavigationBar", + Child = new Button { Name = "PART_BackButton" }.RegisterInNameScope(ns) + }.RegisterInNameScope(ns), + contentHost, + new ContentPresenter { Name = "PART_TopCommandBar" }.RegisterInNameScope(ns), + new ContentPresenter { Name = "PART_ModalBackPresenter" }.RegisterInNameScope(ns), + new ContentPresenter { Name = "PART_ModalPresenter" }.RegisterInNameScope(ns), + } + }; + }); + } + + private class ControllableTransition : IPageTransition + { + private readonly Task _gate; + + public ControllableTransition(Task gate) + { + _gate = gate; + } + + public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (to != null) + to.IsVisible = true; + await _gate; + if (from != null) + from.IsVisible = false; + } + } + } + } From 425346612ce76dd15c21731a057f152ed7fed7ce Mon Sep 17 00:00:00 2001 From: mfkl Date: Mon, 9 Mar 2026 23:41:43 +0700 Subject: [PATCH 04/24] Fix pixel rounding and visual transforms in NativeControlHost (#20786) Ensure that native control bounds are properly rounded when UseLayoutRounding is enabled. This prevents alignment issues with Avalonia's visual tree, especially at non-integer scaling factors. The calculation now uses LayoutHelper.RoundLayoutValue for each edge of the transformed rectangle, matching how Avalonia rounds other layout elements. --- src/Avalonia.Controls/NativeControlHost.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 8328e0fd56..e36bec652c 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Avalonia.Automation.Peers; using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Platform; +using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -152,6 +153,18 @@ namespace Avalonia.Controls if (transformToVisual == null) return null; var transformedRect = new Rect(default, bounds.Size).TransformToAABB(transformToVisual.Value); + // Transformed rect should be pixel-rounded if layout rounding is enabled. + // This is important for native controls to align correctly with Avalonia's visual tree. + if (UseLayoutRounding) + { + var scale = LayoutHelper.GetLayoutScale(this); + var left = LayoutHelper.RoundLayoutValue(transformedRect.X, scale); + var top = LayoutHelper.RoundLayoutValue(transformedRect.Y, scale); + var right = LayoutHelper.RoundLayoutValue(transformedRect.Right, scale); + var bottom = LayoutHelper.RoundLayoutValue(transformedRect.Bottom, scale); + transformedRect = new Rect(new Point(left, top), new Point(right, bottom)); + } + return transformedRect; } From f13bfe6c1b34a00c999601b4df288a9becb6b0d7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 10 Mar 2026 09:01:04 +0000 Subject: [PATCH 05/24] Find perfect match before nearest in font collection (#20851) * Find perfect match before nearest in font collection * Update API suppressions * Address review --- api/Avalonia.nupkg.xml | 16 +++++- .../Media/Fonts/FontCollectionBase.cs | 45 ++++++++++++----- .../Media/Fonts/SystemFontCollection.cs | 15 ++++-- .../Assets/Inter-Bold.ttf | Bin 0 -> 316100 bytes .../Media/CustomFontCollectionTests.cs | 46 ++++++++++++------ .../Media/FontManagerTests.cs | 24 +++++++++ 6 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 92f41c6606..146d6df001 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -1375,6 +1375,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) @@ -2773,6 +2779,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl) @@ -4969,4 +4981,4 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - \ No newline at end of file + diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 40176c88ff..6e4283d76e 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -168,7 +168,7 @@ namespace Avalonia.Media.Fonts var key = typeface.ToFontCollectionKey(); - return TryGetGlyphTypeface(familyName, key, out glyphTypeface); + return TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface); } public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) @@ -455,25 +455,25 @@ namespace Avalonia.Media.Fonts /// find the best match based on the provided . /// The name of the font family to search for. This parameter is case-insensitive. /// The key representing the desired font collection attributes. + /// Whether to allow a nearest match (as opposed to only an exact match). /// When this method returns, contains the matching if a match is found; otherwise, /// . /// if a matching glyph typeface is found; otherwise, . - protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) + protected bool TryGetGlyphTypeface( + string familyName, + FontCollectionKey key, + bool allowNearestMatch, + [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) - { - return true; - } - - if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + if (TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out var isNearestMatch)) { var matchedKey = glyphTypeface.ToFontCollectionKey(); - if (matchedKey != key) + if (isNearestMatch && matchedKey != key) { if (TryCreateSyntheticGlyphTypeface(glyphTypeface, key.Style, key.Weight, key.Stretch, out var syntheticGlyphTypeface)) { @@ -511,7 +511,7 @@ namespace Avalonia.Media.Fonts { // Exact match found in snapshot. Use the exact family name for lookup if (_glyphTypefaceCache.TryGetValue(snapshot[mid].Name, out var exactGlyphTypefaces) && - TryGetNearestMatch(exactGlyphTypefaces, key, out glyphTypeface)) + TryGetMatch(exactGlyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _)) { return true; } @@ -549,7 +549,7 @@ namespace Avalonia.Media.Fonts } if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && - TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _)) { return true; } @@ -559,6 +559,29 @@ namespace Avalonia.Media.Fonts return false; } + private bool TryGetMatch( + IDictionary glyphTypefaces, + FontCollectionKey key, + bool allowNearestMatch, + [NotNullWhen(true)] out GlyphTypeface? glyphTypeface, + out bool isNearestMatch) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface is not null) + { + isNearestMatch = false; + return true; + } + + if (allowNearestMatch && TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + isNearestMatch = true; + return true; + } + + isNearestMatch = false; + return false; + } + /// /// Attempts to retrieve the nearest matching for the specified font key from the /// provided collection of glyph typefaces. diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index 3c81e9890f..1c79127ec3 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -29,14 +29,14 @@ namespace Avalonia.Media.Fonts FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName); + var key = typeface.ToFontCollectionKey(); - if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + // Find an exact match first + if (TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface)) { return true; } - var key = typeface.ToFontCollectionKey(); - //Check cache first to avoid unnecessary calls to the font manager if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface)) { @@ -52,6 +52,13 @@ namespace Avalonia.Media.Fonts return false; } + // The font manager didn't return a perfect match either. Find the nearest match ourselves. + if (key != platformTypeface.ToFontCollectionKey() && + TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface)) + { + return true; + } + glyphTypeface = GlyphTypeface.TryCreate(platformTypeface); if (glyphTypeface is null) { @@ -77,7 +84,7 @@ namespace Avalonia.Media.Fonts } //Requested glyph typeface should be in cache now - return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); + return TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface); } public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) diff --git a/tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf b/tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8e82c70d1081e2857ada1b73395d4f42c2e8adc9 GIT binary patch literal 316100 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�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

6=3(=QnPeuLDdti0n3-y(nd#Q;7fbjCmF@fSxxm zm>11U=4JB=;sL#CW}DZ{>*fta271fPF>fPI%)912GuOOtJ}~ple6zrOXg)F@n@`NA zX5r%B9skOFZN4$znnmV2^S$}O{AhkMKbv37ujV)NyZOWXY5p>Qn}5Op5qiSVhEW)Y zF7$}ilZIK?CTtrn6}AhP4wng+#V^_~AFdFt7`8_&pOwQ^!d1i75YMMWSPGlM=CEVf zDeN4s5q1gJ47-MFg=>fFgzJXuh3kji!tP-?tc2CDC9H)#!k%HTuy@!e+#uXA+$h{Q z+yt?WHVgZP{X)bS2`7w+^=nw+*)ow@2iP9TEL#XNj>A?iTJI?h)=8 z?iKDG?h_(<4C0#%4u^z8!(oVzG(6ldWQ3uE!h^#@!b8Im;mB}QI652?jt$3!Cw?GV>tq=ic8@sLD&Tfx5h&$Sy?9O%`*(*?rVqJ{p|ks0DGW4$R2DDv4`3bcBCC;N82%oqd3lvM+}|A z?GcEjbCf;W9%GNS$04@j3HC(9Z8#Y*r%pvgq|@yg8iVQ_doH2@o^L1E3+#pVB73pD z#9nGIvzOZ|?3MN^d$qmBUTd$j6A_v521E_K36Xhjv9}@y*X@YVbEmz_-fi!(_aY+1 z{q_O-pnb?bjM!Y0>|{H|K58GcQ|&Z6-9Bz-*eC2v`=ose(K?>7&)Vk@x8nuG?0Ct( zY+tdn?5l|M^O}9#zG2_AZ`nEaZA8{Wv>7|szHdLU^XzDpr~S+RZT~?;7ZZhu=Mo_biHm%cNF*1;acPT4F6|JnWSMB$B_g^=WSBgv z3!R<>%TE0qDoYaTB2IiBkCFTih4(Vq79-Aqm81C zqfI0N!{Yci^{A1@Puda@l(vqxiMEZli?)w;h<3~)ELCMi)gFGs1(!c8IQwu8OWkB%W*k8_goR^}jJEqWhxzqX#7VL-er5 z!I=_08a;+cIMXB+PBa6taAqQ!%u~_Rh>7!T^c>>iyb!$@y@ZG|uSBz=SEJd{Ytie7 zA@gSR7NX<4jR-OlA!lwLBWGSTKUxrd7=09d9DRcLGz+88qR$a)=F8}-=tvW2r>UIi7$;WL&Ur*;w$5;;;Z9p;%npU;)(I~@eT2f@lEl~@h$PK zh^=>f9=YQ##0k77z84Yq?vEdcAIxJ~JQ7ceCnM_KqlmIM711K5BR0p3_=$KX;!r#l zKOH|4KN~+6KaZ$9FXnN1UWsSLuOcSLYlzVE24eKQ70*GOo_FGR5m{?4qV;@$xLxxR zx$8qj?)X?D`Na$4&*IPHFXAubuksik-y$BxcX>pQA2lw;uM(L8Q9S;P|5_Z$BSb8Z z2$6rB^Ai0BQ7hWyaVy%nrQI@aS+|^9-mTzPbnV?r5)DM+S!isKCfDpbx=yaMTf=p6 zYr3v(Ew{E?$F1wubL+cquDdI{imSR7S93jFPuI)!c75CiZbP?`+t_X5Hg%i1zOJ9^ z?^<2mHQeTI3%8})%5Ckoaof7>-1cqvqC~lA& z?1s3ZZkXHG4R`yw{oMiXK*TFM*d5{yMNG1hc~r77Zmb*U#v?k};fOkRBw~*p?T$gj zvE$tFh&pznI|*^fPC@js)7#lPX-SzGUccZ(>-Ry30w<6xi?d}eDC!&Phjc6hFy893<oK%Bbnx=yT{!O_k^42o^(&Sr`mwga92TGW)MvhpZ|j%x?flYy z8NaMw&M)s*@GJWEekH%MU&XKLSM#g;4!-1@e6#Q9JNeFj4d2DD>AU*1{MvpUzph`; zukXA0?!N3RzUo_i&G+y z{xpBOKf|Bt&+=y@9^$$FJb%8Q;4knO`iuO<{t|zwzsz6mukcs;tNhje8h@?7&QJ8$ z`y2d?60gtS;%`M<#@qcJ{!V|FzuVvA@Adcj`~3s{LI03{*gxVY`N@8Yf7Czbr}}A# z;qo{l={@0R`X`GxYW_L@ynn&J=wI?LBck0b#NeBqN8x+Jzv9YnQz4-sJA zN3@rDe!gGeKlC5@kNqe9Q@;?gVLs0z$b99$_TTt#{UZMzqQv~*e?-K-pZzcXSO1&; z9Z~iEM0CBs{Xa5EnS_Z&?6Wv=h=_-yS5hZYKMB!OCSua^X>6Uaye8NgnO&lF7*wM4NjI5q+j5(-HA!M)CwA z{5*-cgij+<;m(I6ki=vk=v2HX<3mp1dKEEt5GC(^6tpN_5KPgJfPZ zKUt7Rb^Lg7bj8n;FOn~luad8mZ<245Mag%`_sI{*kI7HT&&e;zuZVc~pLmC9nA$W- zY(l%+^bg8smx^%isx@@{!x_r7qx?^w4OH7 z&C@N?Ez_;it?UDMst-P1kNJ=49?z0-Zt0qMYWP&zmr zk`7IWrTeDC)BV!@(*x22(}U83(?ilj(-G;&bW}Pz9g~ht#}zT{(j(KOmW*+ho|vAL zo}8YNo|>MPp8jtXFhu-14>7+cq!%DY(?y8#bqV5pU6x*scuiL_rWfKiU6)QwuSYbe z8`GQqH@epU#@0$7Odm=gmiSlcWQll{K9){Rr=`==$I}_<6Y0$K$@Ho8>GYZO+4Q;e z`SgYK#q_20<@A+wR{Cl>JAEyEJ$)m6Gkq(alfIq4lfIk2m(ES!M?BDZ>HKs-`eFJ} z`f>V6`f0i_{S5IczevAKze>MOze&GM7p32&-={yMKc+vWKc~N>zox&Xzo&nsf2Mz> zf2aRs2o9Bnna!ds&RpiRBuld_Ym>Fjmde^?OJ~bu%Vx`E%V#TOD`xGpm9mwyRkBsH z)w0#I4p}K{%9^u|S*NUXwno+^TQlpLt(C2vt&^>rt(UEzb<4VE<*brbvzDxu^~ic= zy|Ug}pKOC{!)&8$<7|^`(`>V>Z`LpCpS5Q7tdVV=ZINx6ZIx}EZIf-AZI^AI?U3!5 z?Ue1D?UL=9?UwDH?UC)7?Un7F?UN1224;h@!P$^(Xf`a{HyfVqm+hY&kR6yElpUNM zk{z0j$VO(PveDU?Y-~0z8=oDP9iAPL9hn`K9i1JM9h)7O9iN?$otT}Jot&MLotmAN zot~YMotd4Lot>SNotvGPou5s}F32v-F3K*>F3B#@uE?&;uF9^?uF0;=uFEE7 z*Jn3mH)c0wH)pqGw`R9xw`X@`cV>5GcW3ux_h$EH_h%1e4`vT#4`+{Lld{R#lt!CVlzDz-@jlb5^l#<)t#xXh z=T-Ims-9o1_LRR@>O5Yl_s!3P=IKN8^q_iP-`+gGZ=Mev@2~s$eJb^SdVN2=zMo#- zFTcLr(ud`0>CN;SWv16C_sP>~l$ma$+)wwJexsb1r_w0%K8-D|$`rdk7Z@qtSy?<}Le{a2i@8bUX-%&2!r`(_AtkkuB z8vJ{$RqMH?cA@=YcePKM*HvrugGz7Osa9b+Eq$ov#eeT5F9CVvr^amRcOC*rJ?2SPk$}fRQu<4 zMt=2v{aH`t{+gd6or>n8qIRtCx=KasmG&xE3wu>re|27m{LruJybk-U&w54ep#D;) zpTJ*><65r%>c9Q9p4dJsb*#MHi zYN$W*@8}ltzeDqMp!wgSc|M`}bRoefm|6{T2L>ah>Y)##(?_MzW3*e_HXeKg%Znr(@jCBc zuCN@igPzyF&^$e~t2{l}GtURKuous(6zQ~Ry)%8ZBc5L^+PV5;UE61+Phm&qt3~^V z7VZBVEn4mtEqBrHvY)9mTJrltwcIUQ?iMX~t6tx#*SD(OTGhVV&TEVPD^Cypl&1&H z(}U*e)zNWDzX;9S4OIQNqW;T%0>{;V*?*`+0t#d48aIexQ1P);sp~{uRA{ z(Qc}GfBG{X*Lr{YHTL!X^k?kr{aJt5*ZZ>`u}^!HYg(@@+HTmM$}PR}`Ih9#r)K?KP%Vh_vKdhla=E6sL!2B zjqShE(0uVZSE;ifg%-z)a`S!_wR26MKQ;Or#u0hF;I`~HYueA&igwYfXfK+M_7mkA z>!)1w9J1+)yd(ujQ@u&~{vuyRP}K)817+uhG6~pQ`o; zRqa2j+8w==i3l&%IjloYVW% z*e+0?YCo12k2m$h9$Ky*tk;@8_bd9GY3ZB$U8SLRFZZAymNlIUuSa*I?XU;^zN+KV zD*GqcS?$#$&jBnguePg-w!ccN z_78milq;HkRmY*#Vm!&~EA?J#FSc{+YrXZ-a+NGiOqvP)~ z{RHXidA(?-au3aCasS@=b?DZ$9(!xP^u4Oi@hIv`^QZ4Mb+#)!9%zr6zMr(Po^d|y zUFk=^Dz|Dsru|xl<;CNyxNlJo-7m(E9Dh~n+8(RgZ?*Jg{k9b2=f2vGdEZK-N3oyZ z7yG#%pnb6%je4Fx?9<+jdVW6Mhc&-NJLsk5=tY02^kRDTK593&x=h zX&3b0#eKEiwX$7QIbOi^njg(qx#(B>(jHZ{OGVSEuwA3vx&1JF()=|FJE|WozOSaE z&zqW#A8Y#DsTI$uqF=4^{7Qpb*n*W)evUS2EaowVPsaGZ?cD4$Q&qMy-zqRMeB(p5j#d71j+^z}H?!TTul zjRAlDT&k)cR*Ls$ZU0sF_jv7Ke#<@Shh?_sa#hD|Wqm%E+3w-5^xH~tAAMi0aQu#8 zzqWgh*U^5MZdL76)%U(SuS0&d-Rk>#UEkB|`d(Mpd8In%kyaqm_$?vA#6c^(C^-K|s06 zP6AHCn^g57Unv?hGY@0rnZ!Ggb`pFE!8?mOPeYwf|6b<96fc=HVvU_KGM_iba+Udl z>h(JLQPs+;77ulOcvsj-;r?0)%n#C6WAQll^Yoy(GoW*#GuCwSuG~{Am7N2eK#kAN z1L@~~$MxE{_2s9kgM)H0IIHJ>$Mt-uR`jK+viLe3JQV5bWMNesQMGV>PBx&DwR7R% z6AwkL4?eWu#Cg8ZDe!)^Vh~w0GInmLNA0ZiC85em0=#6=ZkYAV{h~!1RZRzxm0~ij zr#5;{KHxg7AAMNXX*ax#sogc*YB8uQ22J|XRndn-Rnx8Zq}{4|UR7TjE9^9JKW&sn zXH~q+F(0_E){jmK)Qe7-jiRoDW~g@h9Moc8JAHO~*w;><_QSq*`W!r9U+qZ0#Xjq^ zrjs4zq8z>09;!XrKB^ofBA@D4>?H9rl-mpS!}=>X^txU=UMc*nuxByIRJ&G+N!4P~ zPG8nB8zl9K^tJu5v%`LITP#P++X?L?PZz58 z<=_Fg)4y|&fPL);bTX*U!8nfV^&I?Q5S^D7nqQB}KIW%|oid*F96UF4GNGaM+0e=N z2J00sj~whYv>qBdxUFgZR(rAi)$}E$R`i?t(pA$zW?5e{YFrdReQ3SXZ(#?v$C|d6 za^Zja(qAq7s2DWRkKn&rA4UJ7_p9olwyJ~EDqj+CzV>UyOH|R%=%5~NkE~xlJdrQ; zHyw1>c|RP_?TPfXo;i4~my4H4zMNn`PY7_Sx&ZSLcxHundp+MoBseqK&I$5@{gE@I(jQp>3?k7d4G zq5jw&s>Ps7C(){UUA4FNuf=mv2Wyq$IamxL*ni^w>c4Eqc>JpW>g04;{j97nW0m50 z$Hg?voipE6U1TX2i#9qLR_5RxFTdK3wV$hKzfn> z!Bw>wB=LIWU+vpV+bjJN$MgH6UFzT2Ua+rz#(oC-YLDVMuKj9RUoxvY`BN`m+KQL3 z;(omgd$4^}iu`cV3@^p%2dpRT>-}|7vs}Ei>mpyBi+flDW4bLmcqc3&E&3AB!bN1foAJ5TqA$A*_1~6aQB}vG4YmWU<>dK<(!Uxy zxzx~h)zC?%hPJN;+XdFj^ZP(q4~=5cqL_5h#p{N?gf}>T#rdqiM$sPmQjd2>j)PnD zrMIPMM_T@x_OCUyZ*B2pb>1K1-BQQ3#rRfVT56m;!@FZKUdio_^VyzjI``xo`4A1gFC zNrB;UUN2DkYeN^+8#>w1;35+K&UV+(@m@nG(Hh!sG;}hpq5VfgC)FD4Z?J}t*B?~V z(T^P(I_cBUj~*I2`P0yk9~!KON`sR`n3GUH(n+=!t+$q9@`?V4H6YekRiA@Z9nV&^ zpQ!5dx}l5l4Sfl3=%jK(pYsh}9B=4LZ$tg5rT8eL$XDV2T*ShnDEoniF3L9ay{(~( zvkiTJYv>|vL*L^XI!W2k$+m{}Ck>r+Yp~zIFj&W5>|e02_SMDChAzT4^yRvti}4Lk zCSVaZuLsyu`xCybV-85)m-IcfrC7Ap#g2xKTN^rw+0cHw!S@=>U9g{S=%Q9b-(wm& z>D$o7kA{w88@dS6(8Z62zTCIy`)f--^<#cqfbpr?RVRxZ`rh7BOvbX@IFJ26i+&{3 zq90YX=(wOo7nfReeBIDRi-wNB8+@O_B&xOxUCe0cxU`{@%?*wdD-E5*Zs;UpL&w<- zowRJ|q;^Bc!7Vze)1s5(fEMTdrt(;B+?(9rQ(Ll+?$I&N#|{6|B_ zZw;LcZ}547{#os(A1O8Xe8F+`Cmnw`^rNMQPOdiioW$#b+GFt|k@_8<%b4Gw-D^7E zP@|tMr_bFik*ty-~&s^i3(j_Ydr9$w?*WVy!i2GY@fSnIW>9}m^^y}zdKb2VLL zt*M=R6#cBeXV-L6xu%P%HGR(3bds&6^-%X)%1O~#`ji?pY-`vES~CnO|2MT6?WD4k(y4{*Yy3SR?L&=_^PJkj+!n` z)O7Kwrt@Dl?JsMbl*2Sj?iZMo)p3wMAItiYNLlStE+!?}A7NT2uMcQGp2VbNUJmT% z;|6Hnzu;xj*8(+`q7&_xsq_>zRM-7svB{0{i*+1Dclu zTI5gj#pfx~E6%51>ih+c>-bbB>vd8c-|+CcQz_=v)Sq>79e=0YD*Bw(Np>7(KdY1M z*w4$0_qcp~0?qRU&Fulr^99ZGQP=llXkKn;o)2iAZ)k2OXnucaZZBwlUubTBsNR?T z6!!K0#rtUSo?5&Q^SuGzzVUg0hMm9vVzp8fYa{g6#r2_ntcwCwo!p1=`J#5kb*y*& z$PC{|>ECr6Rp#>=*Qp=!IgNe2zrMehnO}SaQEr=m%vbH*Z5Sqjb*Ik~J3aa>xzoDD zPOl~Z(rd}T)XMp3T4cINxmah_uB5C(e|1r;|EWoIh_~3qIh4S1l)5U16xdf6`G{JtoG#X7Dg=>GWWjRXwXk&Dip9A@CZQyEmGtHV+QBuoR@tjNK~s z)g6oBpH6GEu=yZ!+O6s1YVlgl;_gF@!|5@6O3d!ASPm$fNwGYk-!NfGiQQ0Hhe>69 z>=xZ>wPhzJ8f9no~s{dP&xZ{#LJr?iav?Zz-8Cc>k)qfj|WMzrJwDwwJr?KQt zzslmjV5NA;R@#f%VI$Lz8+6_bwUqZeP^}Ohw_^cEo&Voj!-tZ26+`>D@f8f=`3%s7 zEo77pPe0tmKKn3yh?Sq-Q-vdoaxrtnCkJMb^5b=VqCktPFPbagG>a#kevVm1*7J6u zGl$q`u5?Bi_M^Y+G8T2WD>2E}7s6!91o@j}=2U|rUA z*ERkBR#$a0xe!qI=kDL+^X;DQ>FKVndiCC`SJlzRCL*;e@~BeZO*e3hOi+bUnB{BS+*d{SP+(i7)6l~2-B<2aRW*)nF^ zy$)+PUs$Vr_v(|B5B7WKQTgo+7mywr-@Ez1D0)|pSiASj+Rb0qZu(d&6)jtOY)g4o z!~EG+>5wffwpICiJ(05KgZ*wgSgU-HJumdytLw{_E8FV&GNQ${y1qATL$+wyukt}g zZrE1&AS1?XyK=x<<%8_8VOw3_i?qv@<*2Z%$E?-$WxZotU0=0`J&!zJ^|+{frrx>d zB|t69Q}qn6?d_Lxsd^6B@1_eos^my|#+W#zMU`8k9;0A%o;*%HUc)sj-E@*4?oT9}ft~@cSMy5NeB7RZzSV2@p z5~C_Y7gfWCqiVQyR7EnQ-tZ9>nTx7nrBOAkD5{24L{(%ksv?$AYp$*77L zMpXnesyyn ztcLX8xy8m|GPwgw2FSzYrs|*NFqtYf7@xyss`I_8DQ%rE9XyU#-^<#}wo0Y64*Xu- zg|rG7adm#_l#5i?xjQ7?-DoM(U-tz!QMbXJ)NOD(bsOB1yCHv{^#ymAeNg=4D&vMSs=S@2@OFf2ooAUgq5Z zC29So=ESp+lF;AVuiEhaRoi`pY8Z`B>F6(OJYC4r!uFT3N*+{@5-~uwJ#kIdwv3H2 z#70W!K;=viP!c;pN$d!J0>kK`{jMa zqy=VM=9h}Sv8}EzEil{a`qBckt*$REFx%?-(gL$B?^g{p=Q%Q!4pr_k<(b8lXBAWB zA5-!WlQB_#r_!PN(A5|wp2L%t*Bi^EG!Xlx=aDZpDdY2`=b;`LW81xN*3$B-F-L4G zPg=H2884^KBh!tCtP!+Mw+719Ai0u_r08(DB^y7{k#b92VU*k|aaS>Uy_mdSOkOW0uNRZoi^=Q7_r zU$tQK)kqChyXdKQuS1l_{T(B6-1IO4(M=z%akumt{wlv$(#`R1EhF3_kBh1HsX<*e}x`QP+#8$9f}P&ztH2%~vCb zVrm3N)EkMRMqI{JWFcSqnK5sqk_sBcRC_;PT06o}ZaGsf$}jZJ@8yRY$)~gmfk}B> zO!eT$ynK>ck{;WUtxs{C)^StMndZ5*qw=M@H!5GbkrAonh$qY9kJff@yGn)*ZJDhx z<<7-aU?e7k8{wF8|6DEAKm|ntkeN9MY$%xy)~jfV`iG8nqu$ z)ha5T74ozH?&HpxJo(g9$BjFE%6U^Ko;l$>)jHv~N`zD=OVoQZMnwUm((xg!DzBw; zN*a{v36E$q?t-ZkCrz3-?z~ecPU5_+Yquwk^BO@hZ%mI@!&RSA)YG6+RJe?HYbuN6 zEs#v_a!Nx=e~9B<&4mV18cd2B`=v&T$d)ccoMcgY_SPF46IFd*5jEyERy#IK-L86& zK3{3Bh!?4lej*yg%N8kO3<8oZIO&5kM#uFdSSwBAoyQvkp!&EX%HPOWktS~}S43&t zh|(kxC5kb1{+MbxN7UGii0WI+mwpkUNKbPr5m9~A5$U&4g`|i^R9{@w8$+b}9;0eZ z3l5xnUr|-1qqUK8R|C>wbMu!7xjZhWTKF*;WTx*<=2KMlnZ&#?RZ8PURsKZP*bwD! z(8VIlF)IB@x@z*1TMdj9!u#iFpI#q`5#CRH1ks*h=3-J>gLu zJNe##OXakBPa#CS4lg-Cca$2Ki%`1Pqb|5N&3-Ag-T=OcbP~}wGE?%^0H8?i00Q^s z5KdVKqh1|U<|wKDx;`+fF!2E$N6d? zWJGnuN0lAPS4t|T!nXOUV>jQk3DWLSK1wOeMk)5vl^g1Zn_qmd^>k3g8sQBONXyr-hP zr@Os@`brr_y!xOX0El=4MwMG0@y@3nAc%PP=edV!AZkPnSd6HFEfF;!DWV35MAU%1 zh&SL~b!bObk6l!CphwjJkf<5}5>*33qNOBqW)o1CKG5$d2S5z6TsM1GKHE=sB zn=c%v%1s&NsH!hfuYRa*ov3FV)By6RYQ99(z?P_LHbhl-QPlJQWIaYsrBRHkf!5gfFAr6_^@?%jZaru1+9TF(d9!x)D{I%Tuy*wVYd8N{yXDQ= z)oZNXdd=E(npnGfm9?wCS-bU>wX1hntLsa@H@{d7+$&ZC_lnhnD8({h%J=H}YCvDH z%y0Iq>&yIRTU}r3Q?}LhrL)Ply1uk)#J<%1d)=5`x1@A__+H(=)OT#F`&R?$i`7%m z#cDu(v3d%+SPiT%R!^4~s{!`K(wXKw>i(s^Aoiy6Uk!*WRs-US)quESDMuWq?nlZC z+v(VUx|{h5+z?HUN@LZPl?wZrqWX)G3Lo+YaMN>sj=sC+Mx`Oa}Nzf0tG*_Lu&BJ+dmyK;noITkN-M|BtUtTQBbNB!$m%|6D?E_`^6XRLk>#=t8m9=|4)^5JC zcCW|U%~#g$^;o<2&)U6D)~+66?dC6QH(yx0_sd%O4Qh6nhj*&P)mCafdGtf=Egsg@?C?pF>Cw>EI%?nV!7G)l;!8f*DM|0+3aIBc!MQV!m@11o0WZL zTa$D$(|b3TpO~L$K4u1Q%d%?@X=cvv{aNCJL*4rRw2-iNRpopT1u2{{v3o}I&+ z+L)7j3QOkU*8I%GeF2{@%(;lq%)!laVNQkSV-9N08R!w{!E$8aB$lTHPGNa*U^dG+ zfjKO%3|z_b>cDj@ZwTDL@}|H|ESaB~Sp}B{${9W$vI0JJXvmDEN4Siz1V?Lk3yM%pWmSdJvd2g|gS&hk0 zW;P~2ncet8K3|-BF`qBZy_C<_=Tcvop_sfCa}%>0Z;k&QqlfQhv%T3~@4#H**XbRFBoH~re@O2icqFh&A0gyUKZJR!$LV84x#>rX8J_iH#0<~+am?=gu6_dZ zJ8#raWRB&1^plunxvPFM^DFPCkImaZZ-4!iygqq-^aS%ONAy$0yvq7`=2b4x&tPWd zLj6op8v2C1S$VVciFp_2U96wQJ?27-B}+A5i}@lK&634O{Ga?kwOIaNxVXXp!s5jj zw$>IF*Z)u4kSJ=soBECS+wEFc^w`#~S_fMfv@WgaG3D0A|EA^ltOjlpB8Rw-r5tCEkC#;}$2|{h{3- z7ANJu_|)!>b{pFt*Zv07jr_l%L+8meI?S(s-VW9Ex0~CVJfnWD@90*CZ@m8>|K;MR zIu>_4q2mdQCkYl$T3B3B-Eq>M-&Rz2viBO>dC}gdRaAFL?lZKaI#?puvHyME@cx4( zi$4#J36^)&gPXknuKJR~u06W;SW+nd7k%7yK-ZPxf6>R?7Ij_Oty{N6-4-qSxViuC zJ)8Ppa!up^?lG}%zkfCUX2~^&kEy?RaY}A4PRaj;k;eZOJ;Xk_Z2WEZo{Ht}{~>bS zTn(t`(R)Up))hVaY^jq7Y8wB2_YkQVGssKLw%2_pEKWr}%s!^#kVIecANlU^F{AH@ zwjO;){eSP?4*n1zw(qS1GFd+OGcXH1?^@Nx9@*xZ6Y@Ok6p8U2sw|L}lA25RD) zf%69y9oYK7H{9>Ux8k4U#dqR=a7kf1`DyWr?EfX#RP-qPrtk_;mkZYvelw)Ey!Ro! zhm0Okd{9rhMJ=ya_r;b-*bNoOj+!ct5zFSJL+&5bSLW@QK`M`BPH!A@!}0OS z8$Fpjx#pBf-skOEpK^ozUl>_(#3|KFj$rv={eQ79(RWgnSZDvAvZZb-(U)Ve3@&0R z|5qedoT{HXW?bJhup?)5g1L)7pU}FZx}ti*$_ed+_M{FKFZy^wYQjoct`$A_w473Q z%S+fG?8fI4o)y~@HtqQT#Iq)RH8FSMZWDJ~oD$0>{yR=QYvOs^@qfix*PJ!qEA6w- zn^e9%`74f}bl;@McksVz@?ndgTGC7WPcE5!7}C6P@rsJ&vi^vz$s5JerI-3Nx?;K5 zBe%p-)PRcRORhnFQv5!(;|J3wLr`$7T<&-z4Y?*%A^hwh% zonAIQHGS#yOb`6o0F1fxLlOHV&1p%|I+W~y*;}NXJC8w zcivucM!9d^+n4QeS?6muU$gnjnk#FrYIXH#3yZHl?V8P3PZ0aWKToTvX$+K2)#k&9F+|Tpp-NAa*yl1oT!@DD4hwC(%sL>TYuGMb& z%B|aYUc%}wc-OTl#S4$9s1`O)_=CbOQeU`RYC^U6Or2RSZIt+}lo6$?k3c@2;`0%% zl=fK6??mlc{5ikl6E#e&`RrPxCA}(^qKA7$IKV5k?N8y2dmr#l!S8AKO%rd;^IsrZ^ZuE%=3Dw_(VE}I zpX8l+d-*S+9Y5MXk2ld>=6{km%dPUi#oOLq=bd`%d3#$oVZRNi|?`r$r7{GhlbYq}sksAfPm#wui*xcQ0XB6^Iwob-S z-o>_$F;cv1%_!krYf&T4+t&IU2lKYILB=7xZEdh|s5!(OVjRXB*M=E~o5RiF#u4JJ zYsT+*>spC%B=22|8>7XW*NmgYo7aq^dGp$6<5=FbcA{|tZ(18`oG9L`W}L*E)y_4> zn&+A48RL16+AQOA-lBG~F@d+JU204eZ%{MN;tgu$#w6a5w$PZxo6%Mnv&B2mj56MR z_Ox*WZ#;X(xQ(}%WsGv(T=tQ1J8v%AY}~Or5to-DUbk6u~q^6u~sb`<%=`fVViAdEy;TW=q;}FPXdWwx(Ci_98-H?!kMK z+M9dQ?%&hwBwGCDUgAASW@pj9H@ncjFEP7{_PyDSHyj;qcIRzICzw5WbJ3aRzM@TU z_7v?wGsL@y=9v47w-1@Uc>B;zW^djhDYOgrAO>6pjZIkwb zmegL`cnBVcCtwvksaZJ( zYRQ}e7z~9l1ct&eD1zZI0!m5IcBV}~ED+ESqusnlOZd8~J9vZjdd>3BCH1r6YAvDd zu3d$0%DP`)f9=ECdtSxoC*e7G9$tVo@FJ-E(EqJ{=;eufwt@y{E66%q0cR`VYz3UH zfU^~FwgS#pkae~K?`*zKyo)=*d+8I}NGOI;Pyz=*9LB)Wa10y^$HDP%0-OjZ!O1Wd zP66?MDx3!6U_6`-XTX^-0Vcv(a5hYW$uI@Z0SnHBsW1)BgWtn+I3H%f1uzpXgUjIx zm;>axb|r{hUjPf?Zde41p#qk`Qb4}6d*EKU4=Q0f+z%_@0eBD|f`e-qwA%}a&;d(jWp~WbN0Qngp{{qiJHLQjg zf&9un0VvyC@;#S4&n3@u?*#Ik=yD*QE@AKOuImdaNYmQTDmSba<)|8o*ti^dBbCTGmmDr}0 zxtBS~+{>M$)}_fajCP(k$AUc5$>vRbz8P+Ta#Fj1^+H$#OJHfu=jMIvuLO0aoJ!~U zoaJyoRKsd`-g&I?`;D=5?fb z9cf-in%9x$Z?kiT^sXbl>qzf9(z}lIt|PtcYOmVOx1KzH6W)Ti;T_0;D1&z)s~mir z*!}=Mgpc4e_%nRr*hZczr?V)RbKpG4F0TeT8Mq&LSmD^eXi98SVv`cPK}u}$$W~=) zH%duU(leWq$WRg)RT35@k)b3qlthM-m`_P$D2WUuk)b3ql*A86Pxh6{D2)uIk)bp) zltzZq$WR)Y+C11!pQPr)K%nNs?e$=s_0+lbT04CSHFGK42bFL?tbhmLVR!`o2#?~Q zJ;wTRcmk^6S@hA#=oNH@=Q(Po8Rq_6AF*ajt>5iq}?wqW@ z?j-eZSpUsA*`MoJ{ydn?=gXYg{>z>D{ww%A2j;?+a1~q)*T6iu7OsObxE^kR8{sCn z8Ro++a4Xyf<-m3Qcfg&Hg1f+m1+WnAhGnGV0VnBy5FUbu;Zeu-r=5BJS4r2OSg&RM z8obWub?^qPhd1FZc-xt8ta6gZldulnKzra~wNw2zs$sc1hIc{AJwrf) z#M`xZGMmqr!R2rT@M|RAM&fNG-bUhWB;H2iZ6w}C;%y|}M&fNG-bUhWB;H2iZ6w}C z;%y|}M&fNG-bUhWB;H2iZ6w}C;%y|}M&fNG-bUi>ZAiS30~?9Akp&xBu#tEhiMNq> z8;Q4(4I7EKk$C$@B!1^|@u$eZ|9$EYY9s^SAP2w3^4|#!*FYESaQQZ>-=(#+B3m0h z;FO|^OVP!p=;BhPi_2UM^`|T?^*W!|!5gq1-h{W{ZKn*$EJYWWqKiw>#idQ8ye&4Y ztsCdK7%e6=*(GqPtI-^M^yN8az826DTEQ-O zHE}1Q9}HvQSQrbZ!f9{@%!U=tt>SF{HcrAn(n%Qmk!R8?D6JPYn?sIWNlsi_vx!_u zs@7APXg%dFauQl6U(1>mKlzHYYPR5CZ2L;$UTyBx=3Z^?)#hI99k{a9%)QoG(_cCT z&E@v?r@)Pgl2bxrN{NjOCvG(YMnW-+f)Y3g;xGn|hGXDZI1Y}76W~NR2~LKwa0-Yx z*r{+DjDzuTI-CJ#!UUKIXTjMp2`0l7I0wZ4xiA%`!Flj|m=5Q|47dPh!ewwdTmf@n zE?fy$!va_ccf%rB3>B~hmclY1?n)HRB8p}aMYD*aSwzt+qG%RTG>a&jMHI~a&jMHI~>W)VfRh@x3U(JZ297Ev^d zD4InS&GO9#%9SXZMHI~RC>V(&!dETVE2Q8`P`zy^31-h++sK5T*y;6wNbK88;K znIS4?5tXxu%2`C^ETVE2Q8|mKoJCa560Kb#a~6>~i^!bi=Nd%j==a0p{Wt3$G>hn* zMRd+0I%g4`vxv@FMCUA`a~9D#i|Cw1bj~6=XBnNKGaze3=Pb7+oreW#1-rlz@HBlG zN#xF>o_+zTfg1M>NDcJg z**~zat679?>JJOZZFEzu-B`xwd*EJZ9Hq4M2igA+)Q?tL`bzd?M=V#d{UnIUW}4B2HRA z^$C^wremx!^$laKWu!jQAoU4SpHQiHW3Qw>LFyBvK0)deq&`9FMNHN*T4_mRS7--4 zfIG_htCkGh3-^KOf6sG!#zds|L3jwfNH13xw$ALpQSE%UIov>8<_FvHRjD!b~@E|^O z*)P}&{p+$eP4z;5PYg|HnIGu+@q+;-{5SMOoApXj#9p`?#wpsZ># z7XTKP7W-N>&RW;f4rE;jL*PoNv%D64g2LKb^a)z@30m|CTJ#Be)>hanF?P$sVu+j* z_DWb=i~d22{y~e>dDd1~T8mU#`f6fcO;}skUeTH{JNPtOI%~E32B{XKb-YxIktf?u zHM!)DB)FG#({~_kUHv<-mBr1z1JB}G_zS|~in~atyJ*_#X5Yo0?p-i;MC7XvJMF_x z`_drBl?Wd}rZ8FS!HE2nc`5U0lNu9;RQgXtR+H4SE$|I|@5+OPl!{iFC|PkOdE!c-Y9bFmSkvc9$#K<<1EHT+elJ}ieXKSsOR||ez zBR8(M&Dq?hES9+M=54N5e|JQSH&v%Q|ETt9+KS0Kx;>>b3SZ?QIN0@BES|mg#!J&8 z7ZHCEd9NMi{0615K1(MF3p7{yMuFKxJg??AEwG0x(LXD(XDjiMR$|Lm;(4sZM_P%G zv=Sd_B{poO);jB2VvMf1maLDDy4Mk7IO~r~pUt?(tFgFwwZ9ZG12IbK(fX+p{ukcW zaMCjZMnW-+f)Y3g;xGn|hGXDZI1Y{nqPD-<-XY>5zuF!mPj)Dktezq<-ul0{r%1%0 z1|XXQVGtY$1uz&2VF(O`VK5!ehZ%4ITnIDaBKQMb3`w{IE(Pk$kM$Nc+dFg<5`Qzy zhg;xQxDCo71@uV#NY9Yh&PXU&J9sBKSz@KZdmTxgo?IM_>UzYMRy&(mup z^uSK_8o4n!p{J(eH8<=z5}x!A_)>n@`CcPSvev8JD z|DV5?%IhcJ;ip%W#j4C)f)*!`{#Z_JJUDg>KLtdceNW6ZV4;><_)* z0O$>UpfB`;Fyuo7q7Z}rFaQR^AUF^TU@#QI5Eu%>K>QEqDkESd6vHSefrB6pW8i2w z29AZ};CMIzPK1--WEcylKmtyM(_kEohtuH`t2i33|>NI-@UpB<3rw~!T5WZ}PQBNU8 zJ%t$c6k^mh*3`=Mm>cX^%P>%Q;1PdAx1rg81)ol)KiF2Pa#G< zg&6e|V$@TJQBNU8J%xOGLl@Wwg3uMZL3ii@`$A9H4??g%^nwGRH}rwN&=10p4-tq$ z3dyYlnPS!L3>dKQ^QfhY>ImieVJo1UJKcxCO+qw?R3i;4ZLX0W5^O zVG%3_WTdujN*UpEhVVH<_?#g|Duo!S6k?=Oh>=PmMk<9EsT8998e*hUh>=PmMk<9E zsT5+QQb>On-h++sK5T*y;6wNbK88<#a%QAbNT;mz&*3ld1$+r#!Pi>IFa6CB{$|L( zMk`{pQi#z?Ax0~O=)DQybB6FaL-?E_dT&DX-h_;nfXpybDa1&n5F?dBj8qCSQYpkp zrI67XkPk*Gh43{)Xx|XNW(fZ>BwD-pmm%W!p&YK0!~NuNKRMh_4%cL)QizdCAx0{N z0_11lIjDx!@FI|Rj8qCSQYpkpr4S>PLX1=jF;XeSNTm=Xl|qbE3Tf?kv^QF`%YUp# zTHAxOTJ&*S^l@ACaa;6pTl8^T^l@ACaWg6b7Q)@I2pBmhMtyN+i@t7)zHW=YZi~Kd zi@t7)zHW=YZp%G$m@|htbC@%SIdhmZhdFbYGlw~Im@|jn@py|^*PS`cnZukp%$dWS zIn0^EoH@*y!&--(Ni(@2dN4#E{WjD6TOU1|{@ynvzvk)itlnJigw+C_&>k3!`55R-)5IhWzz#rjJSP758 zq&WL%?3p*lOx#xriS=5D+-+7b_V)J1$i9&Jqu#t& znHcrv^&d5EJql^@`i`>hr#9_we{a3C>*hqBV4dR7e!&t2v2j6cTo4-<#Kr}&aY1Zc z5E~c7#szEZ<|00eXR#Q0ffWj3A^GAqwetr{l4Q-TPGD2k0MyVem zy4G1i8|9ae5dEgLe)F9Zzs;sTDKLk9!so3&_s}|KB55%95WimrTb;+v zzr(li5BLuL3IBrc;otBB)PUnWo}+;ebnt@#CgeZ>av={|Kuc%^yFhDb18ref*bR1v zcF-R7fDX_R_Jq!`H*|qsZ~*j%KF}BXK^XEO0#Vq383mi0Ye~!V<_>I_>qH0Z<_~OD z?_1G6$Zi*SqvI$mcSIciy%sQ5J9CZK;63=lnVTDguJDL6SM%eAmlA0z6*JG!Cvh|$ z)a~q9z*>DnE5sY=n{=Z^E9p%ZPxH^>nS5p(p_L=%aSUpPH)^_e#I$%)q>Oez8Rsmc z9Z*I)pp14v8SQ{F+5u&>1IoBg87U|uiYEO3GW`EC{Qoli|1$jlGW`EC{Qoli|1vGW z*=){Ya~7Mk*qp`o&fq?oV!KB$s|nX@eB=W6Xq)TVTu(&nY_4Z>J)7&K7AP_u=82%Y=z zgCRzmLkhHMZC~gK`#}iyhhA_1^oBmr7y3aM@*x6Ih(UiC00UtV90&z4 z7z$ws425A(L>V_RGXgS_pv;8aB`C85rJJC16O?X((q*(XEQGs(*$sTFiMBouL{oiV za?Y>dYuEzFi|-rw8*GKY!?*Ac+LvqK17=qEo=9gaW>?r1+Cg_WQkyVK(P9&k>nUK> z*ygz~lNlB+f>|}2kc|Ygkw7*Q$VMXfddJGW35XKs5+&9i&YoGL(X1MCYBss^=!iK# zUL_?>%$?-lz{^TD5l8+IX&h0r$r$MzXB5|z8Ka;C4uUuw42Qs>a2Om8N5Jo(6pn<^ zw4l$1Nqn9RQ{Wu1;9Qsr)8IV#Jxqu5VFp|P7s5=q2xh?_NYlmSR+4mG!uI8)je9cg z;Pai3g1f+m1+WnAhDER#DqsmPE1$6p?ty#ZKB$D{a6hbo2jD??2p)z%qGeixmc(tzfQbIfR3H%* zNJIq^QGrBMAQ2TvM1{GmW)sqoL>iJvLlS97A`MBTA&E33k%lDFkVG1iNJA27NFohM zq#=nkB$0+B(vUx4N0USi8LgUh9uIE zL>iJvLlS97A`MBTA&E33k%lDFkVG1iNJA27NFohMq#=nkB$0+B(vUq@e<7s6ZMjkcLu?w~sNmOgG^P8Z9V!W_oe$^i!-~qio(nJG@6vQVpe!eWDx+ zD2D>dp@2LtAdjhWZf+XC{oz@5G37S>pG^0$EeEg*jj$ln6;*Fq{pnhHo$0ck2A zO$DT>fHW0oIfh0mu-tefWv0c^=fX1NR`)x!^p_DA3t%~dSPl_k6tQEsugh%1Z}vip z{rpl$s2iz8+TZ6+{=in&UUj|?oVG~0&HLFtAoko&ER9JVJsG9|Ge{a1oC{N78k`5e zhv{%W%zz8vLYN86iD=B?`pf9=7qQb)EKjK>=G?AL6IZvj;ncGPwxG-?b~45&D1n0@ z4hO>_a3~xGhr~o}!wPr+9)ySBVe*gpi^$&u5|BXxo}t8<4U?HJo0!9U?&@ICw+ zet;Tqw4ON{_<%Qh<@mt>6LKH`xsV4fpe3||U7$6zfwr(K>;}6-J7^DkKnLgudqQW} z8@fO*H~@M>ALtAHAPo5sfhZ$)4o1%jYg^>Xfp>=%b3C34Gx51E0^xhpyMXS=pnEdt zo(#GtgYLG`!r4{a@FBWAa+D~oF> z3u+4Wl|Bn?El<_2<*E9$JXOEe?WszlJ((j{%kwXG=Ax^@lxCPS^rbZWQks3;(qw)# zb2U14XPy;TCgpkCD4lX?E?5RK71J;zKjoF#)mKC!eA6_6?TK&p&hh` zJ)i@0ggv1X>;;`+Z|DO10CM3+F8s)aAGz=&7k=czk6ieX3qNwl`ga^XiV{K$nLx$q+w{^2kJ#?hi552wQ!a3)Lu?6?0cp#9{Z1d{=e z#eWW1a4xK=S?k|Wv(_Mu%=l;-q|YFI2I(_MpF#Qz(r1u9gY+4s&mesU=`%>5LHZ2R zXOKRF^ckejAbkeuGmu*Yxiyem1GzPZK@p6EVi*O;GTx+xH)-KbT6mKd-lT;$Y2i&; zc#{_1q-B)CkuVz0hDk6ProcI1!MQLMrUCMX|7+p@TKK;f{;!4qYvKP|_`eqZuZ90> z8ORvkoMl`A<#0RP2`ON-9UiKMhic)WT6m}y9;$_hYT=<;c&HX0s)dJY;h|c1s1_co zg@sop8l;dbO(Pkp* zDUQa&67kOqb|R8kN+hwANMfmv=%DXKcnMyHS0D|q!g_ZUX4v-@>$l+@$iN187v6)7 z@IGvU58y-i2tI>9!xv82PsG_@0ZRby-B0VzPqf)jwAo(?#6bM_!wPr=h;sNvyl^Ev z29LuNunL|8THgMrp$eXXXW==hhSl&q5XJVt0;Gw^XDM%B2sem+mJ<0+7+lZzlAJ9? zDkmb9v~EpWx8@W+GvByL`_H8Phc{J)H&tcQ)-xZ6CtwvkNwoezTI&Tc7z$ws425A( z1jAtjltOki^?pXMu5d@OCUQ%Vk~86Yr=W zgs#vHxq4~;kuh%-}-UQ`vJ5LE6MNQbDel~J+JT+lFHDNrOZ!9@FmK+^Rj*cZq z$C9IC$lS!gB;&19jOEHsmaX0p&s7MjUIGg)XR3(aJq znJhGug=VtQOct8SLNi%tCW}%NWB8cA876CMZ?f_Z0UfUP1ZNa8KOxb~PF`vrPTyUg{{wn)9IcGlXDPLuqqG&oK7}7))4u0A9JL$K z>Llk7eK4iQ+313cfoQWhCte28QKHcF)^YA4&RxXJ=Lz#ryoAGREavpyLNt^}7Z$BO z+O6q6%nj@(o`8n3(NH%2$rfzj!<@Ae-y~xaS26q7ypwZAO)>}Vg2eL8L=_A$AqN7G3wgjS;pi+Ion@o5Y;+bq&d?Tih24Pp;L%w&I?G09+2|}A zon@o5Y;+d=4$v9)27E4bmW|G`(OEV+%SLC}=qwwZWuvofbQaHv!2Zw+4uIa!2l_%k z2tz(ZAPO<)4+CHzFgHCq%SLC}=qwwZWuvofbe4_Ive8*KH9bj9Pg2v9)bu1ZJxQGm zQhq_K)Ao&lT}>T+9$tVy@%y##8t`5dAMc3qy#edxlO|X#3#(;ewJfZbh1IgKS{5>4 zArlrdVPUl_td@nI4(*^l>;WC1BkT#CU@zzldqWr42N)@VWX(mg<|0{hk*v8$)?6fO zE|SI5WUxQYh2_#IzFVc`O4GGhbFbxUQ zkT4Ai(~vL?3Db};4GGhbFbxUQkT4Ai(~vL?3DbOEV(Y$wuVD-P6~2ML!B+S?d<*|@ zE&f~~14v`ol>;;f(x_!gW7t3w;8TT#L}CfT$VnKf44V_tms7+z3#2kf>xkv;m?fne zQmP@P38Yl?+6yU7Af;iXG=Y?ckucdUC1P+2YkOm)V@R0@|Y4DK-A8GKB1|Mngkp>@W@R0`J z7&sb^fn(t~I37-b6X7H{8OFjXkbqO+G$6j?BfjG!zT+dl<0HP~BfjIC2xr0BFbO8Z z6d=Civ*28q3e(^`_&wmkQLA@Mau)NO3RnV50q?*^i_Axh%y%DD!g9DDR=@-BAUp&Q z!z1uVcobH`V?gW8_XN;-^F0Yq!P8I$&%m?r98|+pU=6$o_}}^#&dr{++vWz|1*9E$xO-1W}I5R?vUS{sk;QkEm&*1(H?$6-<4DQe1{tWKV z;QkEm&*1(H?$6-<4DQe131ed%jEB?V3^)@ez(hC;E`dv7He3dC;Yzp)u7+!19$X98 zK^a^RH^7Z>6Wk2*;TE_RZUgd?m|oI&l+P>SF?a%=hAMalo`vV28eW8#;AMCP((o$$ z3D&}TcoQKeGtnR#PS7mKhTnSQ5X!H zoFHwWVBTlU^X(_~N!lnjZIL2kasNO2_qXBWi?$fi86r(hx5*Z=|87`>Z!P?5+Gx}} zn>r`P$%tr>Xp6dSF??v7va*R>iq^*|*wQq%G>t7yV@uQ6(loX-jV(=MOVilWG`2L2 zElp!b)7a58b~KG0O=CyX*wHk0G>siiV@K22(KL26jSWp>L(|yMG&VGi4NYT1)7a27 zHZ+Y5O=CmT*w8dKG>r{SV?)!}&@?tQjSWp>L(|yMG&VGi4NYT1)7a27HZ+Y5O=CmT z*w8dKG>r{SV?)!}&@?tQjSWp>L(|yMG&VGi4NYT1)7a27HZ+Y5O=CmT*w8dKG>r{S zV?)!}&@?tQjSWp>L(|yMG&VGi4NYT1)7a27`Yw%rOQYY?=(9BXEKNO3QxDVB!*sUv zQkT-yp)_?U9k>#f!E$(>e9|>~!zqU{O5;W6BQ4kb$oa_J>})bWb#632bG|hH%=YKn zzUE(?Ps}fzFFEdWXM>iTvyZbmrTx0+5mllY8f z`b_G4H7&1dT3*$(ysBw=RnzjSrsY*l%d47}S2ZoKYFb{^w7jZmc~#T$s;1>tP0OpA zmRB_`uWDLe)wH~-X?a!C@~WogRZYvQnwD2JEw5@?Ue&a`s%d#u)AFjOR5olQ|^Q`FfMbv8wvO;Kl4)Y%kuHpMJB zVKiu*S#aXaf)m${gX7@@I1x^QlVL2J0^%Jzr^0D44#vaja0Z+S6JR2o1!uz~m<&_k z91wF!o(of98k`5ehv{%W%zz7ECR_%W!xb3H^I#? zA8vsK&KhRSiEDSmB3KL+umqOEGPnosh5Miqmc#w90v><|;URb!9)Ul?qp%VlgU8_s zSOrhQQ}8rY!87nIJO|aVT0e{#Jmbui6KAHJI3sc5%#;&nrkuF89^Qnv;BEL6{;b7) zdD^4C7SIw}!7k9+*~08Oaau5OX3vQ;drq9$bK=aN6KD3EIJ4)(nLQ`&>&P6Vd$R5X zdqHQ|8@j+g5QMJK4Z7ES=)*>mE|o)c&GoVafg90&z47z$ws425A(_7@T zkirh6umdUVKngpM!VaXc11ao43OkU(4y3RHDeOQBJCMQ-q_6`i>_7@Tkirh6umdUV zKngpM!VaXc11ao43OkU(4y3RHDeOQBJCMQ-q_6`i>_7@Tkirh6umdUVKngpM!VaXc z11ao43OkU(4y3RHDeOQBJCMQ-q_6`iW(A5fD^Q%)Xq;Jr;=T(3nf6@-v-tiGtS^Qn zTmqNEY(VCj87R)oKyhXUiZe4%+;=5h1<1Yc8kh&y!gWvv*TW5PBisZx!+f{}ZiU;l zLSH%D4tKzvkb=8_s3GIb3K?fs$T+h?-y&G-Y^F`OnKs>K+H{*~(`}|rx0yEGX4-U{ zY13_{O}Cjg-DckkcmN)Rhu~p&1pWw*!b*4y9)~Aj6+8(~!P8I$&%m?r98|+6zt$k_&bq7<_?#TmC) z$P7+##x52zi&LCgoZ^gOEMz99I5RoLnaL@R@02n+vF;4$EBvSw?ZQ$vL;lE z**pfuvhNhO6L2cqbJ)HXPwhIbqglqRQ`f@{a3kEL6`D8m{Vl-kQ6{rTndN+D1}T#n zq)cX=GU**K7qD+3ECOcmGMT~4T*Bw2u!4JiT8lG_RXnHCH!){9+z%_5!Ri61=JRTJ z9$sL34ZMvv_zqBRImkv1vcarZ@f^yJv7v=I-)V7XzKY|ArkMRI&g@rl{LvI+Mhh7; zT8Lko3f%8(2&{l7oUKGIw+5c$m}*!J&-48Y9Jhx3FS3p7=AHoPKiYPyh-_}9eYc8P zv*OH}6=&A0IBmUEwDndI<=n~)T5)F3iWBYJ$}C!OX3>fh_1yZKxA&X3hZnc~-rlO; zygjTwn)5er4^QhiZ|^s6?>BGnH*fDZZ|}dt+hY{adScaY!dvh*yaO2^ZcY2G-uNQg zZ#L~WoA#Sc`^~2PX48JNX}^h{dK-V$#$RRZ4-xV(V=lu)!oy+=8zVmGlcfitjChbv z-?|?RFd+v5kPCUx0$KvSRYVgLL=zK46B9%e6GRhvx(JAt5=~4HO-v9?Ob|^>5KZKn zDd+?|cStlbK{PQzG%-OmF+nsjK{SzPSD`!5<3%(vK{PQzG%-OmF+nsjK{PQzG%-Om zk(mXdFZ6>j6MI5uVi$3C8N_T8J%9q==4hS2dDvu=f^ehfewB!z=RwKKrZA#3up9f&ZJ;ge3cJDX&<@(e9>53;MyFRYI=wQdGwcmrpcfnfy`c~Eg?k1lC}BoM+l-91 zeUvc2bmv^5XTlxFsI_)wQH(id90_-2lNyV$i8f;sg?t_2jzOKn-CWC<8%Akxg;LTO z)S7zlQGz>768}i}PorN`%+o36CJvI4Ao^kZM#H_ylSFlM6W5JOWBd=jXmumeULXzP zPLo>pyF6=utL?d6wQPH9*evokyGE%RLLZNKu5&4QMZGmwQ=W#96;*Dc7A3c9b{+R= zQ}R2(INvaNZF5ecJ%es(yIK{UZBg2ckQ@1mW@uW;o4-4Teu1Ou^VzY#D;gcXw$Yyd z&<=VI_^V-DQ}Y*OWUl)s*Url3kJRXmSI2%pYabxAEufQg*h~eec*imsTNXUfJ`h$Su*ZL%m~*vEt6V&f2=G zuupSz_q!q|%$)2=`5nE=>W`Z2{-NWwnoaV|q-~J%v3Hf4GIi)rS~<^)pF2rsTJxuI zZlpg`997=@G2S=ZbBuSaSj+2Y@A=t2*URsp?X25zB4@ogsAkQ!YLxiS+3akU$2jk7 z$7!|t60|+PXmm`lR@Qsgedm0IWW6r8oxiB{-@NagzpFiJ?d@-3T`cz1UO^qObM>i- zv;SH(_{iA-Xzj|6{g*?*7jD_UCcSyZ; zT}owbual^n6?F&F)A-B%CDf_K8kue4FM;3ov;WmFvJ_3vv$8d`U}{gFbSkJ>TO6LO ztKHYk2dA_y_p%SE`z1OUyI)sc%x|z0f6~Jswa>ftzN2=yI0+K5uBnrerc!r*M@HS$ z{^19@zpCBgejsJx`g^=BByL-2O?b!EUrV0^+ay!!eu?DS;zL#*yZf71SnhG8-nqQt zx18fRxv$}QcCJP>JioWybe{5U(mP^1)`_N$sjFG_zioIw^}qeU*s7D)pMG_x!qZXm zT+Za|a|+w+Q~_(2o5ZsDKSx;%#=WUO=W+4j7qsx+h3c;p^*8?$Tk=lpuJqp_r)bD# z^ow^V&c7(X$>&C0CyOW60OQqcUq`nlNXxV;TbE64>&u__J7;s_^Evl6-dDG$o^?sq?f<1eiQ4YP zCGerxl7G$>ZVgYc{My@>y;e29ftJWVqV6+#>CL*Wo&6+YPENfTi9C~Y2K%j@J!}2n z;r)J5f6vZuWwnG7JARg$s-}KZ=h-)`pEXV9Kvo)>I-TnkWqqMMd3^qK*504^@#iX= zpO}oMe|~GzyZ_^RFZ_>RvG#((Gp_q{DxIZjD*?Ga+>-^LE#8p-ZDQHdt>WyVVD0P{TdHzU{`n>A$`-$`m!H@0?F5n@rH$pR(7USCzew6ea6#*KPe+8oZcs-7mJ|Q*b+u`A;8`sCPlPzSZ0F zKfTdVXYJU{*4gMC`<*0j3*n>ts6bSCPK`7t@IJF@PR9OF^9y{%79-@0#|`_RQ7 z)onAHsWzVQb7=dzGkc#J+_$%T2iGFqP5m);#A<5)&h6V)TATKqw*NZK>#1#@gZ3kF zNl{A9mtKEq)4ymw^$lAL^7jpWy}GmRWQ(tq`3SU{q_E(eyND6UrQ)2lPD_PNm#NrR zK01jSo8$OZ{oJa#Qk+NpiFUKI$^BF+O8$o};&a2lnliDc`TJ{l{Li=T+|=B;>fcdw zNBuwcz6DOFYW;uhwf5S3zhmZo-#_Y64SViF(D+W zpCrkVbUEcnQpu5Sk|arzBsq>G$#MEQlK6k0XTN)lYnRix{63%m%x8V~^FHfYm%a8{ z>v`7Ndq4Z%%Rju$E$_Fxb9Qte#C69t`fvMVJ97`&jBZb&`WNFZI1ejR_+IPY0UB>zuQ zP5(8uh1Z;4_jkq7$x@>)el*wnbz&RCi~XtNp>DXEd`&A$9y;Nj0_r<7{~rJM%j)3v zoA z5AgRvmH2P8nB2xWUPu4BM#EKaD&oH`?axbfqP>})m-62&b)uc6UsH#}Km6QOo?Uff zc~3)8?1^@fJ?%!Tcj%~DELVZ_`%o$IPV@&mtLcgO|7zaJM%2k#;$-}vs^>AY))Dd2 zBjl+mw0O4@Nnk}0KOw*AiMans-cMUL|H;}_F8gFP{k2!Wa^Am=K3PBgb-Dd3Q=Pn~ z|GC!trRRHO1^G);{=4K)wm)%rKj!8WKI!)Fs@cyh=Y-F^{>-$$O3Zx=?3785TNR#C z`d=2wZ+M;d>#{$*_t&NSPoz40@ZVSJ;Z483tizulN&7Q;(GlTaMV)Nj^Q$uZFH7`) zW!?I#?t}g&ZB6s{uloCy^Tn?!>n9WaR;xkivebV3PZs$L1P)bt*W?!@`#&vuq9^(P z`%dWpgyoWIw!`0nkYW_VgP{H(-WF5gxB4sbXTvAki_Nkl{tIO%{1?g2va75u;hRRD zC40%y@@#pR{9bmDKgfe}g*>F@$dziYdP@CX{ZXw@cc@irgSuaBRGZW+wFUo=sUH-i zOgIivPnycK)l;U=OjGmB3^PN`H-l!Ddd{qBhSWkcYDU!(GihF;{%p1|Td2>?e&%qs z#T;dptG(tp^98lfe9>HN=9^2*&1Rwbh51+W3iC_zYqPESjrpy4wH8{MowZN<%*_jY_SEO=3(S7{B7Kp0o4!~#GY9DAy16+}x6m!j+jUFb(j27Q=r-mZ zx~*<+4%XM{>&!cKSKZYt(KqY9=1|>F|K1#~2kAlPXgyR9H_P=%J<=Sj%XFDJPLI{& z%)9k?eUCXoPuBlq-m4$f)6M_UkLcOvLwb&W!hBRet)Dd?(|^+Q%_sB%{k-{VS2Pb&U>MPg>9DOzTza z4PDLJU~SeR>u=UxowW8_`*prO#vY?<+IQP`>jHa%JyF-P@3rsMMfQXCgSxgo!=9l} zw;#11)phKr?5Fe@_8;wMbY1&T_IzE>e$jqe*SBA>m*{isH|&-ATzi$hN?&AuV}Glg zI;T0O=}Vj%P7U4M$#e4brA}SvOx?n1<}}wWofb|DeT9R6-P*a*xl&*0TgC-~2Q1QuHkTtH9sqDAoZt;y)q&BDMi{;6Gvb;QKwz@WZov z#K2>#U5ji$yRc;vamxC7r(7;Fp^wT$7$aBxPzgLv4O0KecN<3V@^ z&o-vPH+aZ+2p+$y7>^r|BhR_UT+mM#OF_SCyaD=617A`w-ZI`6wT%_{`seA!`^GvE zH9j(Si45Z#c)rdz_RH$R$zlgmKACSL>pb$H!Qm+!*Ac3iHNYmxKE@T;Aa8{`H^eggm6Nx5Ba2fYX1 zgEHh^xfk{MyZk%q^PSue&iC?r$ozn>LHRKM9R%kPzW8J)OIada*(wcR8LXnJh$~dO zY6SW`b)IlkW7SyHQRl0s;9RVli5lt>)m$XirT7ArU$s;%k;CQca&TIyR-ms?J;3j& zelM!2f$+?otp=$(@Ga26YA|Gms3D+>RWWkDQo22=M`4;Hq z=5oZoZLUDz-huEMvReCB)jnp1(f8sBp=%=hs{C!e_n-*hs} z56ln1Uu&*KZXcTKP~u1CM~Gc-evH@+CVHLuiMbKb&>&~kr;%$NB7Q$2HsxkH?0er0}zbKhz1M5?dNuR(ufeuHz|4ZrCjXu@yBY0!m2 zW z0T1fu>5vXVGOWYkMBqpLLLJpn(Ot)MO!Rf%c=?R0yQMOqf#=b&Xl->h#I&2?Yh7oODn>08Cwy1(uZ&TW{d zn`nIH9$wy|eL+L}ibkY;;p-jR7dTdrMGoUMzI+KCJYHO+C+G>{Vm(n$1bvUb2lQn4 zX|Jvy(EkI;2la!%Y4FlsT~F83K|i8rf&PP@4SJ5A1AH95*{ka(^b??;)_)Xd=x6k^ z;#}zOKM8yKkSP#y}`V*wus5ioYJ*)w7Ev$jf z;u8J2{#;zCx9BaRjs8M^0nS#vRa_4%;jiLyyfEV3Jbup2~AvKvG%vKz#Wup5d*3#+zuHez8th!|N9Kv)mpKWRNJvaCN^&xq=< zAzlOhy7dO=4b~^ZBuhfrWJBP4n6M#$6YL4Xv?tmVMK)Oyq6%3OpkYY}MK%OtVMBlq z8$x8jhImP6`(^uOaVA+3A__|a+RA=lT82j1wb*el_W zK0?+8zTow zm(B`&O?*v6)OUgJ0#QizhX|1Uf!7nHag~s;JTzM#1}u*bm`ppu!Z2WA3X1 zn zOQI?)iNC;V* zgaoXFSHM{!|183CDQtx_wiPnjR!G2Bcpb5C$TtwXT)qQb3A@2yyCEV!gxz4U-H-;m z0kbpP4QcXAd{4t*>p{VK_&e(P9c%@~wnA9`2rEIsN@xr_fUE(<)_{aHa0zI#1{CXl zrAYT@vFv)HCypMIfk9B;Mb$pa{d;~iFZCC@O(KTyyA2j*~QC)okEpD(D zzbvK2H{;tL2J3Qs;RCun1YQ0kbPPHbw5fvrHcZR3MB|heckrDLgLQd&N|#r`*FFq0 z9pC%NfL;%R&V+W)FthN*4_C)$n5W^3ABI`otd4wgpzn=J`ra_Z`1XgvTHj=SAA-Km zM-Da3n$UR#(ESGMe#yFDvF^W|b^m#0a}&0v*#Td8RBQnROtJu!d6#*YIL{nojzJE` zS_7@v8mPk7fX`fnuZ$GJCU_aC7Q-e;GhZ=ZNm&LF*aUBa{}wEOBrJdx;J*Wnu2`cd zD{1ryH2Qk*KQ=!Rwz<*V1o|^*@g!^UOla{hkajC{xiYswmn+ugS>|@=as^%f73xX) zJI&l>?m{Zk;y%{m*{sDSt!SZRNPiowzYW&kg{;5NV*Py~>+kxkzt7gy@D-FooeRxf z$eQ~s*4*`3bDzzcyB%xphOD_yXU$!QHTUVPx$CgzKAkmp9et_36q@WZeHrj_eK`=T zGgt#yC8Iv1#Y3#c{rVbc@oKt@?gF2%q|5!fhwcduY4r11qt|1NUYj-g`K;0Fu|{vg z8oeHC^fRH+%V4#TF88x8ug$u=0qgQ}S(i7^q|47`T^?jz?$=ZGRFp*;-On05$Qs?x z8a>Dw-LD_hk3oj?x}WuW5PJPd(4^Pntk>)6d3qjXNV}g?NxPrJ+C9$Ny)J9_xL&H4 z!m=S9k8k)v$6vuZz6d)0J!pN>?pLyQuf^KE2-^JvXzR7m@s~iyuMhMSCl0B}E6;=p8F* z@m$v8xz-=>RT;y2*7}q1Tl4WXnV|KiwH)-@_=-$0rNuW|m>HnU|0XhMS3nr7(GAw< zCTsMIS&JJfEsp&F$Q;q-8TLGT9`rhC^mNwZRiVY9r&xcRtiLl@e^-V6UYXL>O2a1mNN_V5gngo9j{o&8?56M z>vhF?y(;VVh;Ni{6tq5RcOPqaMH4EtEoM)VF0f)&`RW zT|3qI*!a7#()eFvpYbJh?8U}c@)Fq+yLGLwB0mp1dDqM4@&^33#!g;$d8O=$wRjs^ ziObudT_2LSlU@aHCNyF>G~w&=G5Mx^OU{MfdsjXw-@{t@X}M7~k}p7aHB(iguP#?% z)fzj030j}3%di?91Kv2bP+dbSO;wIH=4$XhP#>!qw3ftfAXbskVq4Wd^|;!v4yeW0 z9Xy2nKDaMdOQCaY^&06J^#=9_Gu4~WA*ZQ#NN=c>X2=Yycg?t&Q16k(Q16p|P-~zK zE>%0t%gmN)m)Xi}r*@NOz+NHNhNho11JXbnK)+x;Xf~uZpxFj%z}L)nSmnK8UW1u_ zh1uC$hyN}#mz&qq{B3r_`s*)dcg)t?%^sMizcqWBhq1%h2Mb&iJBd|vhIy;i%j#tg zuzFj)&EH#ntUl&I>lW)4^LFb_>rQi!HQSnN-eEg-!Yr{bvAdWL*dy&x<~)0}J=%QE z9&3*^7tq);pU2p`*IZ~%v8R|X+Yi_en2YUc_Cw|?_QUqW=AUVVnM=u9HeaQ&Wxi%V zZ$EFo0c&}Q`6k)P<_g%!@0#z}>+E&r8rpX=Kd^tae>B(8*fKw&v1P6&OW54t6gfra zC(aqpndU|sVdiHv!pto+!ptvdJegaafzCj48|}N9e|1JWBhBs3{m%cGUpfyu51Koj zhn$DZuQ6^OF?Z1@F?TzQoJHo}oX?%l&Aq;}d}o_~_ciieVD7`17;7H%-S4|!E8leA zbZz>U`d-tTci()z-M(*in(uGEz4*G9Ux?cEFgh9|G(~8E5zrdP_6VI3x+3&K=nFpl z{Nvk_QouG9Wy1K;vN6sMPGOvF4FEljM&6I}eT>9E)RgFhgRxG*%fG~d1aZdu-UN5;E(1XFZusDr)mLg6ZN2GPhl62E|^>}Rfyv;+9z2E z>Hbl?hfz6Jh@y^^wlYRbj^((qNAa)qWU$yxN6w|uG027KpM%juG1}@n6vgujz=?z| z=U>TR~bZgZshI#52 zcn~Wj`u@aw}vC)<)FvMCj+O}$_37d&jvmjp9fqRUkt2Nx0;RP%fMM4UsZ{Z zn&8*AlVjn40>~eYh3Gj)b693~A=nkd*b_!mMn@E;VKkG;|B>{`czmL zP81-Aa6&RVj4sDTv`K|bWj^SHKT*|-#V>GxkguR!PGyXH@@}35YYR4Dq@NTMbtpE` z=u|SN&f#Ad>(lz>J{^hopp_D>7~mLy7nV-gdA zQxem>91=4Vb3FQF?UQ&qF`rty5VHzWFRDZ@Vb1H7I4iu|66+EhA-yiKn$e|yE^bL| zN2*vm^LeM+}Pl8oXV;Gb+lU( zw^qm2BuO@mXE!G6dUUdBvIT1EVFRWcSK?H}7L6z>$9O*_7S=694uy4-tqEQFcuclG zHa6M$Xilm60Y&+qMgYdxTZUkn;2dC zx5eG3%z5i6V^3BSG7CH?4QBUi{|Cy7G`C%^3uV{%F8*1Us>kV zbi<KE15T*_<5606q`4dWb$U@VPu^sZRN69^2t1$g(q35^o4ngkq=|h2C{_nmQ(C8 zxa=6t+Pn?OjpZw2-sZe*9zSnqzMts)svf7Zn>@uim@mO`D7XAv z%B^zj$>`K7U|-Qelzsxtj~~tVu5$i`$iLDRPtgVWbs+zw+Y|C(6&x*7h`H-%&XGRj z@{1N0p`D8slBA3It@0ZY?eg=R6HTRi9GCy!fNrha8dg3FqHz{&f6F>^OZLg{kDNQ@ zcLBO|pZp$`Xo?+_Us8cH^QVBWjHUTwz;V&zPdrA(;~$&$X#N~dbv#Zx3IBM>Bb=ve z!e+=n8pj>SIbP;eocZ~Ski+Wy6&1K7|Md#GGXJOOb(jfzLx2Q_ziT7BafDi*=dP;5a^F zi^dnx+?&FhutHtB=CGP&kRMjF7qB&1M4&q}y8Pp@>v6G^*2|}|{1JZNngay>$YO-- zV2@6vnnZo6=Fy10bPVSh8JxjDZ+%n~GZEycGtOdshH(MoON>hy-(+0LxQ1~(<6OpH znu^Os$y+(~4#wStR5zS2+Oq=D=C#nbygGZWQEM;pDr3#>h*R?bBt?NmbU`{oRzVKY zlyj}^L=`3rqI^sPcWTj$|5il3W>Y;YV?jQ1)*j2REORXF>a71B#2KyP`d7jN*nTJC zA20b!IR*77=i{-+Z9~R_hFp3jPUYBtmM&UWv>fO5UxTPiF`sWGtes1o6#6+9;{uY5 zg)b59VnGwMX0w8pN25C{LI$%gyQzTgo{78De)PSM0nJGIbCwW`uSYAT$aWhgmk+Y_u@M7JtKFPcCVVt zfy^P6VJtL>rg^w1izr+Z%)o`anF*d{&GUanpPa<}2M7^A9bpzzb1UI9;4YxC;I1o?M={5~ z$+(ix9W|-3!`iZNJ-C(Z|H7?>JHT=8Zt%>I+5_kQ>9rp9|CEfo-{tOd^ZM6a1p^yG zrb$sVAg>>BL@MC5jNKS}GxlRd`y&>0PVJ$&whgxfc+fbmroeF(0@oSGwFq2Km!IPB z>JRibgq=kgFYY?4wyfIagCG zo(d+sn>cC^b1r4hmBi6_xddr3HbLX@dgO^-1dI}G(G@YQRZMSTdI!r`EaN8{&sP%; zWV$_}x|T{&H!g3lxTA^)Bj-l z0Lw&KCeHjW%&vlq+v=CtU~;4~uIqH%3lS2L}dwwdPk zx0*7i8F4gUDcytVF~ry7h&G>MWRA(bNIp&+Ih$nU<6QbH#4#QqT1_Lq!DpvWM(OI;r!DXn= zbWNsfGR=M7x|C?$jMMU2oA(lJ-b;M@Bckokh*p!S4(cA3U(U$aQ9Z)^1th5^v*gQ6 zKgcw_BX6jsOh3Vr_rn)~VFsDrM`$V+@!mgZk~AAK-H_=B#p=0?uQDzW7B~wy)<+!m zE#pB#s~(kTalgV7iOrSgscV^CJ$wyi8Q7-*frn^~(koH!N?ZDXWFmoKBG5$xqA*9}A zdA@?iDCWFQ96gBXvx&x2@KgtWMoEsQeB@x}k0II^MX~Zu&L_^2e#R;+(+xgF4dVdO z#&+j)Nb)>mY)=EN`g5(gcd6$&hwjWNVeHSGzDy5cmm9>(+W>Qd(LyrF6nU-8&c=d=6`&N6W3Q48y;EZ>mjxz8(}6I33T z_zugzL8z}_`bDN&5n4PmTaPo{mGLf)Jc`*4n1doI+t}i zam)+x7KmZ;ET>wq=Dm?6c_vroM9U@2;aO4sjXBE*f9OK!%Oi9;Qd)<{j(UeU=9OH^ zl|@j+L~vPB;L&g z8k2~|8xO>JmvI}V#aob!6pMF=fco6Dr@`m?DAu-mKgH_(EYBkv&jC}a!OVGs>Z#T< zJ&z^1ui`yyl4(jD)s*OilYHr*Z(#m(#zz>><=oC?x;M?VlIyRo8 z+T}!>rCh78NK$@9w7HIGa~)_L5z<7C z*e+!A7OguP3;T5+)ZchXB)fO-H%RvHNjRcs_uFqYmi6p;>i{{v=iqv08Owq7ja9(2 zjkUlA#s=Uy#%5qcV;k^XV<)hYu?Kjbv9IUgv+5ZKfoDnCtKZExNvBu;+isQq-UGY$ zH23tot!F=3l`)qw-v5qU2g-um2KMSN2M@pxS?6}DN&VYz?%!KBy8ZTgXUPkJ^<{G) zo)8B%kZo_jqsQ&C)9rT*xLtM`M5%g|+&J(y*+<-BtTxsgn~m+pZaiInNLtb_tI4R^ zCkycYcU{>~UWhlg+Ta=Uu6XaNH$s1e!3ZM|#vn{Wn2InHVXmBK3b_bxTrHQY<$AeU zZa1sS-EyBiglD-eW_D&N8rixrD~Fz zs%GMisd;!JeW_ZG=h4@z&1$>at)TPO4xui&xMSR`bLCa7e|5Gx<2qGP0ud(fwgLgn>imI8R874 z1B|SzMF!J#7~3)aj9fUOaBcMKOoHi9c32?012D2oN&g(AU?&rR*kdcoR;Ny%< zI^ta^KBniS_*4e*=cf22ZhEt8>e$82*Gp%PNy(3K`P^=lzH!BGckEr5`n}RU;@wl` zTjibT!M+9^4k+79_z{oSL})s5$?2)|wOc>k$33d;M;|p^ z2c*ZF$9znq>xHn^oQ=2P$;QtRS)!__hPf~o@2EyaTqH$4Zb%f0+AMof zdh_(o>HX46(;uiZJN?OwY5qY~7FJoFz9fB3#=I&6t87T$R%K`U{wkfSlvO!crB{_H z>HduL^ms-*f@WwgxLc zWQx;p7S(YcHE<>&oJ#~}6T|r=IFI>u1(maZe22}o$KZ9(kia&$q#EL(I=1K3*$m&D3 zT2?u(pOMv8Y+}p+j-hv}vL@ns8d)?a8(DpEO^v!?P*rh6$U^V0n}Z`p5A!KST?7-M z4g%sRJ`=nP;rYBFhf%@A9wLvuO6kJDGJcLuM6Z)y}G$ zRUc_u;a$yK_{ync--KT+@oTYug#v!Hz^@QI=$v8q!LR1{6@d?(Gcjk$tY-KXhF_h! z_RT_O4#g0~m^qW0N9HL2h<1ZPf)a}CSn zgTreGaEU|p&JrqfFa9Bo-!+8Mx}I>VZLQw1ddI5Os`jtiKYK;3erQKaIGO7+r)Mq6 zEX{14**3FRCfX%)yKry~F2x(AH|XxVl&^=(+y-uQa9iNb)RuTJ)xAp4f59$vwwY~E z5|u84?Wx?%3sb+1%yMBOXZ#1B zF=8U9A>LH9T3VM|t*k4o*4C9)8|x~(ZP_lBrmC=l-&OoK;#4uD8UsHxPs(SV&+%UR zEAmy)OYpYc60Gf>cT)y83Ol&?$bSI6A0;MIu}g##oKg8du!ZCEaL$KBhTJOuBDcxE z;;r;ARM-7^{lh3`qtUjxmF{qq1C`T z2Q}#{0(hG_fVY_8rAt@EyUW$#d87tL!caYo-vpMyU66_Egih)_ov&-^0$ocN>LOi7 zkJjb-E{!+6gN*_^vIZqakWCfXb>e?Wy(xmoIf8efV@Q35uB*?)cstXsXP;%?Z1=Tq zf%PMUn?^-p^?P< zw)s{~c+jY26`qI(>3Je9z7jjd*J79WM(h^fiap|QVz2nS_)h$<*eCuW_KWYu0r7)4 zD1H= >Tih9M1Qn1(hi!^R9NgT*2)<1!(WGEe5qny|lX!B1Ne{IkIulRN{q_?faE z{Iu1_d!G&DIk3#nm5uQBXJdK3Y$7j!osKyr^tw1*f2aRf@6-R#`}Ozwfc`-r)IaJ& z`miM|!;)}jWLny?EZcG{pOt1+vC^#!%Wnm&pp|K5S=m-q>olvHRo%+5YFN2e$cn|xMs%Pmx=-C*-bLgFHypcT* zo<){fuaRdFyq)}(wVd8gw%)N;TJKu$@KJ5g&nltMsmeHY3E3?~-GBC4oqYUZ?^Nd0 zfhV2Eenva}6K9d*$Gr7V_PA4>MTM3#&Ri=R2bu+11X=~!1lk8W1+EQr4fF`~4)hK5 z4-5=soHm)&veUA2GiO$9o7pF;S9a~px!KEWOwY{C z8j#sPyG!=Q?EW> z5=LYy|CZT{@vOO#y_kgE89e)GA+w)Ol1^7b)fkXHAh`k(15y$)yK8b0eq%0rP+m$qkS9BIlF0g(`0{enx;4cbX}pc>t@$Y zc3~MKngw1MH0ij8srtc+i@KciTuzoBWjD$i#!}}{jj{${4b}w5K9zV~pz(Ks*Rc|> zC3u^`Yg)mRSv#^CfV(DDD&=S9^v&s;)i?1vB>RHTXPuabU!8=?s+v_b+?si?*AiI4 z!D^FPBHW01s3~kpGn}7OH!%hjr8UFBoZQ4fvP%)@57uMmn^|P5T0}((QoQ*Qv_O{E z0_Q@G^23~;<%C_$k9HFOo)pIL%;^Nh?AaNQdAiZ8+MUa;JfnaR+#%vG7`ndxC%$IN2Raa)7iI=Cg=jJX%3xVW~#mBE!^Sl!6s5^!a3p*;;N_I;=^j^Oec z*k#~tLF`PTz-`5{%fJk)6x@a$1$i0= zhK0T!!1xc82g->CKh@@e4MJn?}a}YBJ|BV8T za@}2tOA(_2(LmIa*zxFw@|i+DxTg4J>K)*>fQ;$Vh-pJ<{5$_us!LH-~H245_HzFh~OY@1O3UUgIFgp=9JU!aqXy zz=Czz5gIA3;kXJVH*MfieWP{I^4f+%E$OgYHoAhV!*z!3%wR{2pYkz^4mcPPB%a?kG z{)#-c>Rrqu&Q~e=RPo=yr^~l`54^Ow$Gv*5{=5F0hp1gw)b1p9+cB2g&$Hdgo+E3n zD(tuu*>;uVJv+}kMy!Zy^QA05;O@GBR3wJyk4MbpryaBe}I3me^{Wc{{ep)bb8BR zNB=ngB>w}LM^|99Jt(G$hs1R8u$Unp5i`Z3VwU{p?CF!PvrfLk!XALP3OU_dhX|S5 zSv;c`pQwiatQDtUF(&_WYlf4pB~P}7RDIR$YB;aVTUkX|xgTr)_u;kM7;lwVu^O0l z)H>iqtE-u(vfZ)kaz@39?k2lW#i|aADTN)75ay2%v}qXnH|$5KhCuU;0iRYt>pq~g zEgO60VSl(PVjCeqmqKC^*a)< z$2-?1R?Yc#f!&??S-9U-)9!{6W+T8>Lk*>|pN>XTq?Mtmp&7s#6p!-S(HUUmSI69_ zLeoOCLQ_Z<{=DFQ5Al9|CSr8xfzXK1n9!t9G3Ccz>IA(0T#cC)zp>su8{1;!AM!N* zOTG*9*bE`AoS{eu2GS zDmSi+P;MPvN2HF_N44DLz2AQ$6))G~Pcdka3z-_c9NX%QKThgXKTho^?; zhkJ)dgd2z3hO@#2q1|B-S{hm%njM-S8jlv;EV6PBgv^jXloLvZ>Vz7Gn&vLfeLZ(& z?%Ldqxm$C0=I+fMlRFV-dE&heI?E~S#q4AfM|MLD>|oGN3EGW!LkiH(YU27AqPKOk zyW2g{=e*WAs*U+7!=8pm!zbiF(0a%3xg4nnwc=X>Z74p4k4WrsSLwl_YZw~JW?+qV>f(S-(Bl(fKkw%ebk=Bt; zk#3Q`kwM|YaJ_J&aMN&0v}fmVw{V|uIGhxxNBTtuM@l2(BM(GoMxKl;h%AXLkF1Go zjBJbSj_i-hXnM3-G#;%TZ4kXM+A`Wc+9ldMIv`pSEssu$PK(ZtJ`-IOT^3y#X%cCU z(mF=EM0!Q)A>XQzXrwSwAN&@PwvlVW?H?(Qltm^+rbcE(o{lVxERC#)tc`4nY>(`T z9DoyIe>5kWjMj-Zj5dw7igt{4jrNHS3~vkX3V(<5G9&5X&1e(qJ5Kgj^S7G)dsnNIIf|IKias{^A-;wX2PcA`E!yUQE284|W zn-H){iEKgGim(k~I|A&j2rT}{E`;3(dl2>_e1`zrHi9u8Ie>uqErR)tR!vci;HZg! z@fCFtXf>6NfO#*9u|q4aC`L~db6_+F0c(_K7$J%fM@S;%BNQMMBGg8xgHRWt9zuPD z1_%ui8X+`BXo7GdLQ{lh2+a{%Ahbkih0q$I4MJOl_6QvjIw5pMxE7%cLRW-t2t5#b zA@oM*gU}bDA3}eG0SE&T1|bYaC`Kqj7=|zcp%kGEp&Vfh!Z?KS2on(|AxuV?g75&s zRD@{=(-CGM%tV-lFdJbG!d!$W5uQeP24Nn;e1ruE3lSC}yo9hAVF|)g(I{@_&dPl{ z?$2GAyELAYy8<)8rrhnhd)#>uwGCYx>J{pbnY%1BF(zvq%C%x?x&GX0v8>!^Zb5Wa zjr};!Q(q&xS6FEd;W;_Zb8mPA(aE@CGCCF4N=5fY55_P%#o(YghWi%emnc>Z$Eva1SRBV_EI(Eo zv)=g_uNPx=))Aic?uO2W-(2{-^~1}4o^d)f>tJ}d9RaVl55ZslTkvB0K0NAubHZ6N zx03aCmOe*c0j)Jj-|Noc&{7Y=NAk1QV(S&_J!`el(RtB@(Z!gTmV*XHS4G!Gu}UM1 zZjNq??gR~t?xAx5&1V#IVt%0sQ)i;-d}0N$I4p2V^7BB#TMch{1}lJ zBlcTl#a@rCh^>yTi*1Z;5q4}tY;$Z|Y-emwY+vjke(#O#j~$Ae@w9j_UQO8Xbn*>E zo`KjekR30G*NNAUH;P{vZys-j_@?m|@z(M7@y_wC@m~0SZM<6?{&~ndPke-M;)CKP z@zVI1_{8{>__X*;aQGOX6Ms5BKfVY%5srI|zaC!^Umaf;--s(}!0$I<2ga)zn-Hq! zgzs4S_!i+y^hpeeuZ5?d&GBvVo$OV|7vB-z9se$VAR*ugJ3Wz=$bnp1BABR_2q%(> z!bIIfgG6KGDxsq<<>N?QioOI((V7MQn07=e(KOK_(K^vS(V610w|hC_tB_|H@(V*= zVaO*8d4x%nASbJ`btBRfofBOXy%K#B0}_K1!xCkQafwNZ2e8K^VXfe+C9qt`tGa?M zbP24VcCg)SvX*ck>>7Q&sKS2Gv(OW#i){43=S19EWW9`g@35^>c9nYv?SJ%a5ll=> z)K4@zaorN7i7`0emHK~y@9KTPRr(*m_wX!%#G3ef;QRUjaE<-}xK{rN{7@ePuG5Es zA7Kkp>J1j`L;Z;b+f{G0V1wyTEep5_k5@_knT5SAy%`VqN&UH%2K>Up4vyYxWdQ$z zeG;j+Spnde7Az3`m4$Bx=v}Z?rT)gs0q(YH0Kc_zfqSeFuvFg#{5w3GOZ_*zjUe@2 zx+j6w`KcD{l4`+BZow@5ENt`GQSC^&`U8#q1i6*ZQt3bI^%m9wxV!r?_DPiL#Am7W zV|t5K1^haCyOoJ!Q2!NAUntz&+F@nmSY7Y5PQx)re{Er|!dlRkYr*HDa7UBQhJ5Iz zY7gJe&*A#hIT+jvOzs8bJ-;2UF7*N*_kwip1=)NKRk`*F?ggh)FZwyNF=jl!s0&Lu zF3!ZtBPr_H*JJ&07Oa*y?qR>9zhle0HeVlFhn&vW$b?Pmo-M6M&fqIX>!G@Q)lAr` zXY!TP7!|m7xC7V%zW*#C^9jaewYg{H{gqihFQp;a=TU_+3cti#u}lap$fr+9?<3g?@$g zN^RWRYY9#WcNNjca1ZZv-0Qm>oG`3vTn*gOI|FzAt^g;3dyeRVxWiW$cLH036U7}! z^hVt8tB3o7ZNQ1)ZX|jp?){yOdxY&ob!eSBu?Ddwn44R}UTq9JvPHBtY}U@OC3{8t z!fqWL9TqK%j)U#`fT(^fCptYkD>@ez?Skk_(WTKhqbs9pqU)oZqFbXoqPsDsPlr8z z5uUkgFMGj*>v;G@e*!ki2f=Ij9QX-;4$r>5cS3#Ut+Ct{ zNDlPE%-CFLg+;NYr>;fxyXb+K5IN*KkUR%QcSZL`_mkg1R}06&v1AP1_RtQE$(J7a z$7An!une$P!yS5fhK7dcZ#U%Vr}R^z2K!*nwfb6p@!U;rydJbj(|F5xTj-8%@jmeZ z&={ph=N5yHy(3yKHc8~hx*p3TkA38?udW z7Bh9WhBR=CVviya;um6?5x$+}>P(MW8b;V)wh>mRy_(C@f@_BA3 z?P$j^$yd3d;m!6V!*X(*8iqrjpN%x~PHR+gZg*}sGMsS^ydlH?>=Q=7neWUuvYn02 zr$$v@zOTTj=Bw?iZPf6c=R42Hg%{c~Bjmfwcb5_Iz3N+LM19MB%Z)hspEVK~Q>{-r zrnVk6syh9waaGw)Jkiq&wJ~?5oX>>Cm+RZh;@j#^1rHXBaf<`6lYn* z7FJp4vrB=m*{=cLv)==LVB?Dk_9lBX@C*A3;5K_3aEHAExX1oGaG(7T;1Bi>KzJ+0 z{VCHifwtoS)0{M5x|0s{JAPoMgKr2q=Q-yAuX3&ec64C>!HYJ&BI8`|V80CBv~K_o zcCapVN*vf*PMK2%T;gDb<}7tkPvp5J-U5E_d=ETG@96lX5BFGn4jxK3eChP8 zy)Vlb1vc zGv9dE+^ws~>X^k(ljrEubYsb0_+?M>L@#?;?X8~jM)L43hgk1hYh|(ZvGs`@W_@mb zDMwm6t#9Qxcy<3?PO?YX_sR#!&$N6Bex@IiPm_mfIp3aXKPsQIAG05m&)ZMf&&!4O z3-%KE8hlHylJ7V@ou2YT@&+x}IqRH{cKXEoYo8?Ad%omfN`Vzi` z+~lj}t0h1475R$fX5SgUGvw#KdcOK{3%-leNdASsiz5F@p3~)a-%#IB`K52TZ@Apy z8|fP<@l6EW1O9DWs=023mcmyHWM7P~KJpe=i=}cfcAlS*L%0RYxdrdy7JLx9%&X)y zZpXRYj!$ztKFjU6h}&@qx8qyfjvsS7Zs2y@%NILc zyuFHcRMia;?ilWh9kzktVc{|1$>HhYIpKNXm%^`ySB2Myw}f|MN9$0;iDX5>k%CCQ zNMr1swTX0&^!TlI<6s?ZhRw1U>vRii^ITX)b+LAB25X=bR;qntgJL7#S9c0lqjO`} zp}-1sHCCKkW4mJe;sR^Ts#sYTVm;XetH`!kJNAN}EsmFAwKz3C3oFEh@ugT5u8nVk zUfqM0oC%MmIarm|Ni@WYtW}~T)>VBH1F@zWlbD>Co|u!Emv|}hdSX>#ePT;uXX3lW zp`??{N`{jK$$H7g$>zy6$}b9`DAiIa!GP|a!qn$a$9nDa(|x8 zOV6v87tgDm*C6jg_+M+E*Cnra-hjN4yz;zBdDHS{=RK3RC~sNb%Di>(3$`O~Z{ERt zD?bPyUitZT^Bd(i%Wn-IPu=qS!k5#C{BikH@@M4F&7YsYIMg82B-A|A8aqN=um{sG zGzfb_<)Mk#gP(yNp=Ux1LrX$$Vn=9QXj5ogXczW`4u;KedblcfQS-xf!VSVr!p+02 zLt*Si7l!I$6xBkYopZ7p3|<9P;eFi*_C1=ylAL5r5_OF4vA=r;TN9V_{_YiW9d^-M zvrTao+Z1i_e#K>?Gusr`;{Hb;(Tn$RZd5~YFQhlGNp9j@_`bX*xrNsx{plW$xC2(+ z7%>DZll#OleZRh6lwz-Bx){YPtueeiJr--~`^0^`ZkfXCmKj)sx{A59<1L=Vo_A02wA~v!-+#n;{;tAuiD9SuBH2UKf{i>{Tmd`z8F35t zX`dJO(|*5r2v1us6^~+v_6;!`d$V7Nx$;}sGH<|ky41+Q*9@*OYN#vK)kYNexvnv4 z;c3a=8HLdP1C1hB)?$H;(IoCMX$cO1$8FGemqjRHtguZVjXTpZ+FCQfvO3rbHIz#2-(p&U$CPe9`&D`Bc8- zeCB*6UvaiLf5Ee?abH|6^Cf)+@-<(fuTU`tz?>ThV?e%3hV?-!7n;KisO8ic#gUEQa;#iL_XF$ z3;E!ANuuj6#U){{d%DIQU-+@r|Fl_P2fYUN;#m;$Z*DCU8u=w2_zG+9nm zU;1-tn$KXR9TVem=-nju94n_g!aLFiZaZY-4xuZ5bV-%D^yI%MbF|dae0tyNh}_YV z$HoZQfR$yBmOeg?-#0i#3RtYi{8Kx@em?HMa=V;3o@~_P(;Z#IlSv5cDfH5-89M+K z?&{JnI(om`@o(&1rH(GY14bUnwM+4FM>zGn9lkthQa*_Bs16mB7fboMu^o`oqr6zk z)#XvX9_7X&Wh#~^HV?mDHj|o(t0@)&y7V~DwZfWXQI4VFK4>@?ZtXI zBae!-Ue3g8XT8a@!&@n&mYrd}g_i4{I(A4MZ%7^6rH);3T%Kx4kD@d#k5ai5rE@7t z=~7UI1?}E6o zH&mp(p`zRyD$?G7`P!AE7WXJGEw#AE^U9^x^>|*|t`%v!R+QVdA}#H!yD7R>q{a8o z^b8xjHPAViGPYp6jIkx-<&3QuuVBQ^3*_+>1F#Jvz6%MuEhC=R0NtLk17kMPCmGU!mQ=8B4TsdVTxDyRB$+ZvGNTmbP^>7oa`gmof|?5)t)2%C zR|}Z`J+N3EAP&B;=W45aQxw(7<)Kzg(M;%3UM$UeE|2CbkMd$^Zn_uGOrvyxD}{HW zF~c}L4D>Xd7y1~^2>lIb0lqasl4QBW{Nwb%dWSeK0LNk-B(2HvJ76*17bgDy0?TRV z7j1<($11}U%hDQ$Cnj<3c(xa@21XD{!DzC|42&#mh=I|B5;2-khZ{H+^9DGWPplFH z^NCezU_L<$zX~ii-T+Q8mH|tQ*MOzQ>%h^H$|{yrN||g094DzJqa>AHF5d?h%hkX# z`5thHq$@Q+;;K146rFXk!Z^YesRb-i7++SY!uZ0~c@$Wz9s^ELvw$V)55Q728#o&8 z$|FyKxzV~CU)MvCqa)FbAV-n@);%Q>JJxm^(TnOf#n9~3Y=XG zc(;)Y9AaRuuqM!|0&hV(bPM8+(9d#@~RW zjJ?1K#<#$7w$O6F2Es@YCb`twrDGy3)*BY=wh`FSf)M#j#BG^6V!*m za`iEv*Z+Vn7WV;1U=I?KVlr@)xF0w~OaYc-zko)~!@x3QI&hppZFsja131K(37lX& z0xUPs&ZrxuDwdOgBP8WqChq}`lE~TqUfv73TvGnSB(>*wNx79tx<;cUwe4_8*JXmF zYg8`Lw#f5S(8cmIU>R2Ebk>`IL*!=Q1X{yEatojJv!ILBAAzIPQ^4WsPrwOk9d7<3iN zI)pA`T-}XTY%-p{a95NP6siwG22x{TW!UUImt@*MOyJ32>NN1stoE0`Gzk zFXSNUdKb%=fFrT*L;Z3QuuMJ&9EC4I`an6q99wVv8l*pHXrE&pqn56zRTJY!< zG@?d|_kknCT40%=5=V&-fkVY|;BY)Oi5iLzfaQWmHNL-%+{9|&FtG+WR=fusEod&7 zEZ#;t4aZ3Aj$g%s+H<715ja9nJC_M+)p4Q+aFjs%W2T~OF;w&f4i~=%P7rjZ%0)L| zi5Lhh6}^DN1YM)C;&$L@aT{Rll=BEld6r4aXPhj-_1z=UZ|%QH^kI9i zq_RfILBP8u`mp_XN%;(wRKwx$qlnTaUHx)NWtGTcV5vlj&UKQ`Wtc>LoEs#15%kWT zz|ry!;AAlf=RFE%dgRiqe zSAMb@4=h&q0`F800!ONQfFsm>z%n%fI7&?i-mPec9IEc89;7gfI%x{?s*|p!0R4*Q zkm2eP-~{+rL><)Kz!EhRSgIxhhp8Emyk1QPJyuNu-lZM}XNjU&Xta6=IN87(;nYWr zg?ubCUf^Siv4D@mj3qb@!`Z%q<7mjWK&oP+74S~uD&R=ta^MK#3SgOW8E}-*1~}BX z7&zSM44h!J2bLR`0!xhF0ZWaRz+pyf;8>#>aJ0$*7OPC)ovIpeqzVE@s4QTaN>Kk% zN$Ni;kNS`DgFi~;Q_oSSfgY-=bIUlOhvTt5YMEScGF1)Gd5A!rvr}V zIKn>x^)RCoA4`mmI8y83xpo|f8+d~s$6>-kNyYROG)5-XZv>uwmi8Bd&aDi)Jvc+` z@ZsFBBZae~r{FOrsjlVNSHk(gp9sze`zbgRK`l6z?*2RDvHOGT4X-KEnuHxIoR9H6 zu-Nzk`EN3Q1b${50&bSn3L}jJz!An_DqZqyE+yza5@|7FQ5Ht50hv(}b;ekwx($_7 zmS1vJiL-DrWg6&mN%bj_bS|ZmuH`UE=Q0*|15iUrXK|OL`cIO$a+pWLXq6FY zj|d-2;2VX0my)*$9?v;^EPmg1#W$;F)qTcUWT|poNA?UiP0R- zW_nL%&OP#MCU*HVW%#&fV91NYk*8qNR24i4<2?s6=E##UYQJ-h1zm!@6~gX}9T;x_ zy8Bh`k>cEaEBENdw@dN6@)0{CsiQ039XmNBkG+u8(UtG6U3uE`p?K_pq>iq9hm<_+ z|G4q4Ja#_#H}*YJM^_$u9;x51JoY>IH|-ix`gSRK+BZz zy7JiDNd0!@v9DphmRi|dT|srPpgL4gH&jsVDkv{4wU(O;_KzwkFDP_x#be&rX|WPmnb(aQEs`kkLjiL z+L?G>TCbgn=cV=9nRY(Cv|c+C&r9pIGx5B%UOOYMBCXfX#Pia6?Myr`t=G=9&)}8o zwKMU&v|c+C&r9pIGs>?>>$NlSytH0B6VFTQwKHm*N=ua6&P2IsiE`VSC^s!pZad=~ zE7E%HOgt~G*UrTA(t7PoyTtDK66KZawKMU&a=msYo>wmFz=#^m)2`%;(2Mkc1CU$X}xwPo|o2ZXX3eOQEIBrM7iY><<^-fH!V?a zTM^}@_1YOVu1M>(Gx5B%UON-dOY5~W&hh_sZN@kV0#OwFOTsa{nynVZMiOt~i9DCB zzG0nTXn{YIOn{+TG25=4VY+sP>DoC}+O;!G*Um8AT>tSr(y?7T!*uNo)3tL*yLN`@ z+8H+8n7{Kf&CX1-Gt+dLW@jc{JLjzJ+8L&6XPB;?bDizl8K!Gzn6903XYJY Date: Wed, 11 Mar 2026 00:23:41 +1100 Subject: [PATCH 06/24] Add automation landmarks to HamburgerMenu (#20857) --- .../HamburgerMenu/HamburgerMenu.xaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index cffecb5b7d..49972bdc64 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml @@ -73,7 +73,8 @@ CornerRadius="{TemplateBinding CornerRadius}" TextElement.FontFamily="{TemplateBinding FontFamily}" TextElement.FontSize="{TemplateBinding FontSize}" - TextElement.FontWeight="{TemplateBinding FontWeight}" /> + TextElement.FontWeight="{TemplateBinding FontWeight}" + AutomationProperties.LandmarkType="Main" /> @@ -182,7 +183,9 @@ HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"> + HorizontalAlignment="Stretch" + AutomationProperties.ControlTypeOverride="List" + AutomationProperties.LandmarkType="Navigation"> - + + IsChecked="{Binding #PART_NavigationPane.IsPaneOpen, Mode=TwoWay}" + AutomationProperties.ControlTypeOverride="ListItem"> From 0f2760afce49fadefb0767b2cff6504a9ec77b79 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 10 Mar 2026 14:43:20 +0100 Subject: [PATCH 07/24] Fix `TabControl` `DataContext` issues when switching tabs (#20856) * Add failing tests for #18280 and #20845 Tests cover: - TabItem child DataContext binding not resolving (#20845) - DataContext binding not propagating to TabItem children (#20845) - DataContext binding not surviving tab switch round-trip (#20845) - UserControl content losing DataContext on tab switch (#18280) - Content temporarily getting wrong DataContext when switching tabs - Transition not applying new DataContext to old content Co-Authored-By: Claude Opus 4.6 * Fix TabControl DataContext issues (#18280, #20845) Add ContentPresenter.SetContentWithDataContext to atomically set Content and DataContext, preventing the intermediate state where setting Content to a Control clears DataContext and causes the content to briefly inherit the wrong DataContext from higher up the tree. TabControl.UpdateSelectedContent now uses this method, and the DataContext subscription no longer applies the new container's DataContext to the old content during page transitions. Co-Authored-By: Claude Opus 4.6 * Add tests for ContentTemplate with Control content DataContext When a TabItem has a ContentTemplate and its Content is a Control, the ContentPresenter should set DataContext to the content (so the template can bind to the control's properties), not the TabItem's DataContext. Co-Authored-By: Claude Opus 4.6 * Only use SetContentWithDataContext when no ContentTemplate is set When a ContentTemplate is present and content is a Control, the ContentPresenter should set DataContext = content so the template can bind to the control's properties. Only override DataContext with the container's DataContext when there's no template (i.e. the presenter displays the control directly). Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../Presenters/ContentPresenter.cs | 49 +- src/Avalonia.Controls/TabControl.cs | 50 +- .../TabControlTests.cs | 432 ++++++++++++++++++ 3 files changed, 509 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index a5079f8344..183882ca50 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -177,6 +177,7 @@ namespace Avalonia.Controls.Presenters private Control? _child; private bool _createdChild; private IRecyclingDataTemplate? _recyclingDataTemplate; + private (bool IsSet, object? Value) _overrideDataContext; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); ///

@@ -420,6 +421,39 @@ namespace Avalonia.Controls.Presenters /// internal IContentPresenterHost? Host { get; private set; } + /// + /// Sets the and properties atomically, + /// ensuring that the content's DataContext is never temporarily set to an incorrect value. + /// + /// The new content. + /// The DataContext to set on the presenter. + /// + /// When is set to a , the presenter normally + /// clears its to allow the content to inherit it. This method + /// overrides that behavior, setting the to the specified value + /// before updating the child, preventing any intermediate state where the content could + /// inherit an incorrect DataContext from higher up the tree. + /// + internal void SetContentWithDataContext(object? content, object? dataContext) + { + _overrideDataContext = (true, dataContext); + + try + { + SetCurrentValue(ContentProperty, content); + } + finally + { + // If Content didn't change, UpdateChild wasn't called and the + // override wasn't consumed. Apply the DataContext directly. + if (_overrideDataContext.IsSet) + { + _overrideDataContext = default; + DataContext = dataContext; + } + } + } + /// public sealed override void ApplyTemplate() { @@ -484,8 +518,19 @@ namespace Avalonia.Controls.Presenters } } - // Set the DataContext if the data isn't a control. - if (contentTemplate is { } || !(content is Control)) + // Consume the override immediately so any reentrant/cascading calls + // to UpdateChild don't incorrectly apply the stale override. + var overrideDataContext = _overrideDataContext; + _overrideDataContext = default; + + // Set the DataContext: use the caller-provided override if set, + // otherwise set to content when a template is present or content + // isn't a control, or clear for template-less control content. + if (overrideDataContext.IsSet) + { + DataContext = overrideDataContext.Value; + } + else if (contentTemplate is { } || !(content is Control)) { DataContext = content; } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index f0c624489f..67274247c2 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Avalonia.Animation; @@ -297,8 +296,6 @@ namespace Avalonia.Controls _selectedItemSubscriptions = new CompositeDisposable( container.GetObservable(ContentControl.ContentProperty).Subscribe(content => { - var contentElement = content as StyledElement; - var contentDataContext = contentElement?.DataContext; SelectedContent = content; if (isInitialFire && shouldTransition) @@ -306,11 +303,13 @@ namespace Avalonia.Controls var template = SelectContentTemplate(container.GetValue(ContentControl.ContentTemplateProperty)); SelectedContentTemplate = template; - _contentPresenter2!.Content = content; - _contentPresenter2.ContentTemplate = template; - _contentPresenter2.IsVisible = true; - if (contentElement is not null && contentElement.DataContext != contentDataContext) - _contentPresenter2.DataContext = contentDataContext; + _contentPresenter2!.ContentTemplate = template; + _contentPresenter2!.IsVisible = true; + + if (content is Control && template is null) + _contentPresenter2.SetContentWithDataContext(content, container.DataContext); + else + _contentPresenter2.Content = content; _pendingForward = forward; _shouldAnimate = true; @@ -320,18 +319,11 @@ namespace Avalonia.Controls { if (ContentPart != null) { - ContentPart.Content = content; - // When ContentPart displays a Control, it doesn't set its - // DataContext to that of the Control's. If the content doesn't - // set a DataContext it gets inherited from the TabControl. - // Work around this by setting ContentPart's DataContext to - // the content's original DataContext (inherited from container). - if (contentElement is not null && - contentElement.DataContext != contentDataContext) - { - Debug.Assert(!contentElement.IsSet(DataContextProperty)); - ContentPart.DataContext = contentDataContext; - } + var template = SelectContentTemplate(container.GetValue(ContentControl.ContentTemplateProperty)); + if (content is Control && template is null) + ContentPart.SetContentWithDataContext(content, container.DataContext); + else + ContentPart.Content = content; } } @@ -342,6 +334,24 @@ namespace Avalonia.Controls SelectedContentTemplate = SelectContentTemplate(v); if (ContentPart != null && !_shouldAnimate) ContentPart.ContentTemplate = _selectedContentTemplate; + }), + container.GetObservable(StyledElement.DataContextProperty).Subscribe(dc => + { + // During a transition, ContentPart holds the old tab's content + // and _contentPresenter2 holds the new tab's content. Only update + // the presenter that is showing this container's content. + // Only override DataContext when there's no ContentTemplate; + // with a template, the presenter's DataContext should be the + // content itself (so the template can bind to it). + if (_contentPresenter2 is { IsVisible: true }) + { + if (_contentPresenter2.Content is Control && _contentPresenter2.ContentTemplate is null) + _contentPresenter2.DataContext = dc; + } + else if (ContentPart is { Content: Control } && ContentPart.ContentTemplate is null) + { + ContentPart.DataContext = dc; + } })); IDataTemplate? SelectContentTemplate(IDataTemplate? containerTemplate) => containerTemplate ?? ContentTemplate; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 62c453a18b..8bad019c2b 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -1046,6 +1046,438 @@ namespace Avalonia.Controls.UnitTests assetLoader: new StandardAssetLoader())); } + [Fact] + public void Switching_Tab_Should_Preserve_DataContext_Binding_On_UserControl_Content() + { + // Issue #18280: When switching tabs, a UserControl inside a TabItem has its + // DataContext set to null, causing two-way bindings on child controls (like + // DataGrid.SelectedItem) to propagate null back to the view model. + // Verify that after switching away and back, the DataContext binding still + // resolves correctly. + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var viewModel = new TabDataContextViewModel { SelectedItem = "Item1" }; + + // Create a UserControl with an explicit DataContext binding, + // matching the issue scenario. + var userControl = new UserControl + { + [~UserControl.DataContextProperty] = new Binding("SelectedItem"), + }; + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + Content = userControl, + }, + new TabItem + { + Header = "Tab2", + Content = "Other content", + }, + }, + }; + + var root = new TestRoot(target); + Prepare(target); + + // Verify initial state + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Item1", userControl.DataContext); + + // Switch to second tab and back + target.SelectedIndex = 1; + target.SelectedIndex = 0; + + // The UserControl's DataContext binding should still resolve correctly. + Assert.Equal("Item1", userControl.DataContext); + + // Verify the binding is still live by changing the source property. + viewModel.SelectedItem = "Item2"; + Assert.Equal("Item2", userControl.DataContext); + } + + [Fact] + public void TabItem_Child_DataContext_Binding_Should_Work() + { + // Issue #20845: When a DataContext binding is placed on the child of a TabItem, + // the DataContext is null. The binding hasn't resolved when the content's + // DataContext is captured in UpdateSelectedContent, so the captured value is null. + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var viewModel = new MainViewModel(); + + var tab1View = new UserControl(); + tab1View.Bind(UserControl.DataContextProperty, new Binding("Tab1")); + + // Add a child TextBlock that binds to a property on Tab1ViewModel. + var textBlock = new TextBlock(); + textBlock.Bind(TextBlock.TextProperty, new Binding("Name")); + tab1View.Content = textBlock; + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + Content = tab1View, + }, + }, + }; + + var root = new TestRoot(target); + Prepare(target); + + // The UserControl's DataContext should be the Tab1ViewModel. + Assert.Same(viewModel.Tab1, tab1View.DataContext); + + // The TextBlock should display the Name from Tab1ViewModel. + Assert.Equal("Tab 1 message here", textBlock.Text); + } + + [Fact] + public void TabItem_Child_With_DataContext_Binding_Should_Propagate_To_Children() + { + // Issue #20845 (comment): Putting the DataContext binding on the TabItem itself + // is also broken. The child should inherit the TabItem's DataContext. + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var viewModel = new MainViewModel(); + + var textBlock = new TextBlock(); + textBlock.Bind(TextBlock.TextProperty, new Binding("Name")); + var tab1View = new UserControl { Content = textBlock }; + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + [~TabItem.DataContextProperty] = new Binding("Tab1"), + Content = tab1View, + }, + }, + }; + + var root = new TestRoot(target); + Prepare(target); + + // The TabItem's DataContext should be the Tab1ViewModel. + var tabItem = (TabItem)target.Items[0]!; + Assert.Same(viewModel.Tab1, tabItem.DataContext); + + // The UserControl should inherit the TabItem's DataContext. + Assert.Same(viewModel.Tab1, tab1View.DataContext); + + // The TextBlock should display the Name from Tab1ViewModel. + Assert.Equal("Tab 1 message here", textBlock.Text); + } + + [Fact] + public void Switching_Tabs_Should_Not_Null_Out_DataContext_Bound_Properties() + { + // Issue #20845: DataContext binding should survive tab switches. + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var viewModel = new MainViewModel(); + + var tab1View = new UserControl(); + tab1View.Bind(UserControl.DataContextProperty, new Binding("Tab1")); + var textBlock = new TextBlock(); + textBlock.Bind(TextBlock.TextProperty, new Binding("Name")); + tab1View.Content = textBlock; + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + Content = tab1View, + }, + new TabItem + { + Header = "Tab2", + Content = "Other content", + }, + }, + }; + + var root = new TestRoot(target); + Prepare(target); + + Assert.Same(viewModel.Tab1, tab1View.DataContext); + Assert.Equal("Tab 1 message here", textBlock.Text); + + // Switch to tab 2 and back + target.SelectedIndex = 1; + target.SelectedIndex = 0; + + // DataContext binding should still be resolved correctly. + Assert.Same(viewModel.Tab1, tab1View.DataContext); + Assert.Equal("Tab 1 message here", textBlock.Text); + } + + [Fact] + public void Content_Should_Not_Temporarily_Get_Wrong_DataContext_When_Switching_Tabs() + { + // When ContentPart.Content is set, ContentPresenter.UpdateChild clears its + // DataContext before we can set it to the container's DataContext. This causes + // the content to briefly inherit TabControl's DataContext instead of TabItem's. + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var viewModel = new MainViewModel(); + + var tab1View = new UserControl(); + var tab2View = new UserControl(); + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + [~TabItem.DataContextProperty] = new Binding("Tab1"), + Content = tab1View, + }, + new TabItem + { + Header = "Tab2", + [~TabItem.DataContextProperty] = new Binding("Tab2"), + Content = tab2View, + }, + }, + }; + + var root = new TestRoot(target); + Prepare(target); + + Assert.Same(viewModel.Tab1, tab1View.DataContext); + + // Track all DataContext values the new content receives during the switch. + var dataContexts = new List(); + tab2View.PropertyChanged += (s, e) => + { + if (e.Property == StyledElement.DataContextProperty) + dataContexts.Add(e.NewValue); + }; + + target.SelectedIndex = 1; + + // tab2View should only have received the correct DataContext (Tab2ViewModel). + // It should NOT have temporarily received the TabControl's DataContext (MainViewModel). + Assert.All(dataContexts, dc => Assert.Same(viewModel.Tab2, dc)); + Assert.Same(viewModel.Tab2, tab2View.DataContext); + } + + [Fact] + public void Transition_Should_Not_Apply_New_DataContext_To_Old_Content() + { + // When a PageTransition is set, the old content stays in ContentPart while the + // new content goes into _contentPresenter2. The DataContext subscription for the + // new container should not update ContentPart's DataContext (which still holds + // the old content). + using var app = Start(); + + var viewModel = new MainViewModel(); + + var tab1View = new UserControl(); + var tab2View = new UserControl(); + + var transition = new Mock(); + transition + .Setup(t => t.Start( + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var target = new TabControl + { + PageTransition = transition.Object, + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + [~TabItem.DataContextProperty] = new Binding("Tab1"), + Content = tab1View, + }, + new TabItem + { + Header = "Tab2", + [~TabItem.DataContextProperty] = new Binding("Tab2"), + Content = tab2View, + }, + }, + }; + + var root = CreateRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.Same(viewModel.Tab1, tab1View.DataContext); + + // Track all DataContext values the OLD content receives during the transition. + var oldContentDataContexts = new List(); + tab1View.PropertyChanged += (s, e) => + { + if (e.Property == StyledElement.DataContextProperty) + oldContentDataContexts.Add(e.NewValue); + }; + + // Switch tab — triggers transition + target.SelectedIndex = 1; + root.LayoutManager.ExecuteLayoutPass(); + + // The old content (tab1View) should NOT have received Tab2's DataContext. + Assert.DoesNotContain(viewModel.Tab2, oldContentDataContexts); + } + + [Fact] + public void ContentTemplate_With_Control_Content_Should_Set_DataContext_To_Content() + { + // When a TabItem has a ContentTemplate and its Content is a Control, the + // ContentPresenter should set DataContext = content (so the template can bind + // to the control's properties), not the TabItem's DataContext. + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var viewModel = new MainViewModel(); + var userControl = new UserControl { Tag = "my-content" }; + + TextBlock? templateChild = null; + var contentTemplate = new FuncDataTemplate((x, _) => + { + templateChild = new TextBlock(); + templateChild.Bind(TextBlock.TextProperty, new Binding("Tag")); + return templateChild; + }); + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + [~TabItem.DataContextProperty] = new Binding("Tab1"), + ContentTemplate = contentTemplate, + Content = userControl, + }, + }, + }; + + var root = new TestRoot(target); + Prepare(target); + + // The ContentPresenter's DataContext should be the content (UserControl), + // not the TabItem's DataContext (Tab1ViewModel), because ContentTemplate is set. + Assert.Same(userControl, target.ContentPart!.DataContext); + Assert.NotNull(templateChild); + Assert.Equal("my-content", templateChild!.Text); + } + + [Fact] + public void ContentTemplate_With_Control_Content_Should_Set_DataContext_To_Content_After_Tab_Switch() + { + // Same as above but verifies the behavior after switching tabs. + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var viewModel = new MainViewModel(); + var userControl = new UserControl { Tag = "my-content" }; + + TextBlock? templateChild = null; + var contentTemplate = new FuncDataTemplate((x, _) => + { + templateChild = new TextBlock(); + templateChild.Bind(TextBlock.TextProperty, new Binding("Tag")); + return templateChild; + }); + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = viewModel, + Items = + { + new TabItem + { + Header = "Tab1", + [~TabItem.DataContextProperty] = new Binding("Tab1"), + ContentTemplate = contentTemplate, + Content = userControl, + }, + new TabItem + { + Header = "Tab2", + Content = "Other content", + }, + }, + }; + + var root = new TestRoot(target); + Prepare(target); + + Assert.Same(userControl, target.ContentPart!.DataContext); + + // Switch away and back. + target.SelectedIndex = 1; + target.SelectedIndex = 0; + + // DataContext should still be the content, not the TabItem's DataContext. + Assert.Same(userControl, target.ContentPart!.DataContext); + Assert.NotNull(templateChild); + Assert.Equal("my-content", templateChild!.Text); + } + + private class TabDataContextViewModel : NotifyingBase + { + private string? _selectedItem; + + public string? SelectedItem + { + get => _selectedItem; + set => SetField(ref _selectedItem, value); + } + } + + private class MainViewModel + { + public Tab1ViewModel Tab1 { get; set; } = new(); + public Tab2ViewModel Tab2 { get; set; } = new(); + } + + private class Tab1ViewModel + { + public string Name { get; set; } = "Tab 1 message here"; + } + + private class Tab2ViewModel + { + public string Name { get; set; } = "Tab 2 message here"; + } + private class Item { public Item(string value) From 9a21a48aa40ef102fe13a00c6b3c2c893b770ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Su=C3=A1rez?= Date: Tue, 10 Mar 2026 17:47:30 +0100 Subject: [PATCH 08/24] [Feature] Add PipsPager Control (#20660) * Added PipsPager control * Added tests * Added render tests * Added samples * More improvements * More tests * Added more samples * Fix formatting * Updated Automation * Small optimization * More changes * Changes based on feedback * Fix build errors * More changes * Updated samples * Fixes * More changes * Fix build * More changes * More changes * More tests * More constants --- samples/ControlCatalog/MainView.xaml | 3 + .../PipsPager/PipsPagerCarouselPage.xaml | 50 ++ .../PipsPager/PipsPagerCarouselPage.xaml.cs | 11 + .../PipsPager/PipsPagerCustomButtonsPage.xaml | 78 +++ .../PipsPagerCustomButtonsPage.xaml.cs | 11 + .../PipsPager/PipsPagerCustomColorsPage.xaml | 59 ++ .../PipsPagerCustomColorsPage.xaml.cs | 11 + .../PipsPagerCustomTemplatesPage.xaml | 197 ++++++ .../PipsPagerCustomTemplatesPage.xaml.cs | 11 + .../Pages/PipsPager/PipsPagerEventsPage.xaml | 35 + .../PipsPager/PipsPagerEventsPage.xaml.cs | 29 + .../PipsPagerGettingStartedPage.xaml | 46 ++ .../PipsPagerGettingStartedPage.xaml.cs | 11 + .../PipsPagerLargeCollectionPage.xaml | 52 ++ .../PipsPagerLargeCollectionPage.xaml.cs | 11 + .../ControlCatalog/Pages/PipsPagerPage.xaml | 11 + .../Pages/PipsPagerPage.xaml.cs | 47 ++ .../Peers/PipsPagerAutomationPeer.cs | 85 +++ src/Avalonia.Controls/PipsPager/PipsPager.cs | 662 ++++++++++++++++++ .../PipsPagerSelectedIndexChangedEventArgs.cs | 27 + .../PipsPager/PipsPagerTemplateSettings.cs | 30 + .../Controls/FluentControls.xaml | 1 + .../Controls/PipsPager.xaml | 312 +++++++++ .../Controls/PipsPager.xaml | 143 ++++ .../Controls/SimpleControls.xaml | 1 + .../PipsPagerTests.cs | 578 +++++++++++++++ .../Controls/PipsPagerTests.cs | 168 +++++ .../PipsPager/PipsPager_Default.expected.png | Bin 0 -> 1498 bytes .../PipsPager_Preselected_Index.expected.png | Bin 0 -> 1401 bytes 29 files changed, 2680 insertions(+) create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs create mode 100644 samples/ControlCatalog/Pages/PipsPagerPage.xaml create mode 100644 samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs create mode 100644 src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs create mode 100644 src/Avalonia.Controls/PipsPager/PipsPager.cs create mode 100644 src/Avalonia.Controls/PipsPager/PipsPagerSelectedIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Controls/PipsPager/PipsPagerTemplateSettings.cs create mode 100644 src/Avalonia.Themes.Fluent/Controls/PipsPager.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/PipsPager.xaml create mode 100644 tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs create mode 100644 tests/Avalonia.RenderTests/Controls/PipsPagerTests.cs create mode 100644 tests/TestFiles/Skia/Controls/PipsPager/PipsPager_Default.expected.png create mode 100644 tests/TestFiles/Skia/Controls/PipsPager/PipsPager_Preselected_Index.expected.png diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index c8c496b50c..b6249fe17f 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -161,6 +161,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml new file mode 100644 index 0000000000..b75b5c37c2 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs new file mode 100644 index 0000000000..f42bb10ce9 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCarouselPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCarouselPage : UserControl +{ + public PipsPagerCarouselPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml new file mode 100644 index 0000000000..8b9856424d --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs new file mode 100644 index 0000000000..4fc74995bc --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomButtonsPage : UserControl +{ + public PipsPagerCustomButtonsPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml new file mode 100644 index 0000000000..260536d7ae --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs new file mode 100644 index 0000000000..a9276f11b0 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomColorsPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomColorsPage : UserControl +{ + public PipsPagerCustomColorsPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml new file mode 100644 index 0000000000..fe748b248d --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs new file mode 100644 index 0000000000..cce9e6c5e5 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomTemplatesPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerCustomTemplatesPage : UserControl +{ + public PipsPagerCustomTemplatesPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml new file mode 100644 index 0000000000..a69c101687 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs new file mode 100644 index 0000000000..d97165397a --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerEventsPage.xaml.cs @@ -0,0 +1,29 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerEventsPage : UserControl +{ + private readonly ObservableCollection _events = new(); + + public PipsPagerEventsPage() + { + InitializeComponent(); + + EventLog.ItemsSource = _events; + + EventPager.PropertyChanged += (_, e) => + { + if (e.Property != PipsPager.SelectedPageIndexProperty) + return; + + var newIndex = (int)e.NewValue!; + StatusText.Text = $"Selected: {newIndex}"; + _events.Insert(0, $"SelectedPageIndex changed to {newIndex}"); + + if (_events.Count > 20) + _events.RemoveAt(_events.Count - 1); + }; + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml new file mode 100644 index 0000000000..5eead2fb31 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs new file mode 100644 index 0000000000..80a1569f30 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerGettingStartedPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerGettingStartedPage : UserControl +{ + public PipsPagerGettingStartedPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml new file mode 100644 index 0000000000..5cc416d413 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs new file mode 100644 index 0000000000..2dc936b544 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerLargeCollectionPage.xaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ControlCatalog.Pages; + +public partial class PipsPagerLargeCollectionPage : UserControl +{ + public PipsPagerLargeCollectionPage() + { + InitializeComponent(); + } +} diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml b/samples/ControlCatalog/Pages/PipsPagerPage.xaml new file mode 100644 index 0000000000..54112daae0 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs new file mode 100644 index 0000000000..8f27cc61f8 --- /dev/null +++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs @@ -0,0 +1,47 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public partial class PipsPagerPage : UserControl + { + private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = + { + ("Getting Started", "First Look", + "Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.", + () => new PipsPagerGettingStartedPage()), + + ("Features", "Carousel Integration", + "Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.", + () => new PipsPagerCarouselPage()), + ("Features", "Large Collections", + "Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.", + () => new PipsPagerLargeCollectionPage()), + ("Features", "Events", + "Monitor SelectedPageIndex changes to react to user navigation.", + () => new PipsPagerEventsPage()), + + ("Appearance", "Custom Colors", + "Override pip indicator colors using resource keys for normal, selected, and hover states.", + () => new PipsPagerCustomColorsPage()), + ("Appearance", "Custom Buttons", + "Replace the default chevron navigation buttons with custom styled buttons.", + () => new PipsPagerCustomButtonsPage()), + ("Appearance", "Custom Templates", + "Override pip item templates to create squares, pills, numbers, or any custom shape.", + () => new PipsPagerCustomTemplatesPage()), + }; + + public PipsPagerPage() + { + InitializeComponent(); + Loaded += OnLoaded; + } + + private async void OnLoaded(object? sender, RoutedEventArgs e) + { + await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs new file mode 100644 index 0000000000..b40a9b4159 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/PipsPagerAutomationPeer.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + /// + /// An automation peer for . + /// + public class PipsPagerAutomationPeer : ControlAutomationPeer, ISelectionProvider + { + private ListBox? _pipsList; + + /// + /// Initializes a new instance of the class. + /// + /// The control associated with this peer. + public PipsPagerAutomationPeer(PipsPager owner) : base(owner) + { + owner.SelectedIndexChanged += OnSelectionChanged; + } + + /// + /// Gets the owner as a . + /// + private new PipsPager Owner => (PipsPager)base.Owner; + + /// + public bool CanSelectMultiple => false; + + /// + public bool IsSelectionRequired => true; + + /// + public IReadOnlyList GetSelection() + { + var result = new List(); + var owner = Owner; + + if (owner.SelectedPageIndex >= 0 && owner.SelectedPageIndex < owner.NumberOfPages) + { + _pipsList ??= owner.FindNameScope()?.Find("PART_PipsPagerList"); + + if (_pipsList != null) + { + var container = _pipsList.ContainerFromIndex(owner.SelectedPageIndex); + if (container is Control c) + { + var peer = GetOrCreate(c); + result.Add(peer); + } + } + } + + return result; + } + + /// + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.List; + } + + /// + protected override string GetClassNameCore() + { + return nameof(PipsPager); + } + + /// + protected override string? GetNameCore() + { + var name = base.GetNameCore(); + return string.IsNullOrWhiteSpace(name) ? "Pips Pager" : name; + } + + private void OnSelectionChanged(object? sender, Controls.PipsPagerSelectedIndexChangedEventArgs e) + { + RaisePropertyChangedEvent( + SelectionPatternIdentifiers.SelectionProperty, + e.OldIndex, + e.NewIndex); + } + } +} diff --git a/src/Avalonia.Controls/PipsPager/PipsPager.cs b/src/Avalonia.Controls/PipsPager/PipsPager.cs new file mode 100644 index 0000000000..b976df4826 --- /dev/null +++ b/src/Avalonia.Controls/PipsPager/PipsPager.cs @@ -0,0 +1,662 @@ +using System; +using System.Threading; +using Avalonia.Threading; +using Avalonia.Controls.Metadata; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Styling; +using System.Collections.Generic; + +namespace Avalonia.Controls +{ + /// + /// Represents a control that lets the user navigate through a paginated collection using a set of pips. + /// + [TemplatePart(PART_PreviousButton, typeof(Button))] + [TemplatePart(PART_NextButton, typeof(Button))] + [TemplatePart(PART_PipsPagerList, typeof(ListBox))] + [PseudoClasses(PC_FirstPage, PC_LastPage, PC_Vertical, PC_Horizontal)] + public class PipsPager : TemplatedControl + { + private const string PART_PreviousButton = "PART_PreviousButton"; + private const string PART_NextButton = "PART_NextButton"; + private const string PART_PipsPagerList = "PART_PipsPagerList"; + + private const string PC_FirstPage = ":first-page"; + private const string PC_LastPage = ":last-page"; + private const string PC_Vertical = ":vertical"; + private const string PC_Horizontal = ":horizontal"; + + private Button? _previousButton; + private Button? _nextButton; + private ListBox? _pipsPagerList; + private bool _scrollPending; + private bool _updatingPagerSize; + private bool _isInitialLoad; + private int _lastSelectedPageIndex; + private CancellationTokenSource? _scrollAnimationCts; + private PipsPagerTemplateSettings _templateSettings = new PipsPagerTemplateSettings(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxVisiblePipsProperty = + AvaloniaProperty.Register(nameof(MaxVisiblePips), 5); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsNextButtonVisibleProperty = + AvaloniaProperty.Register(nameof(IsNextButtonVisible), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty NumberOfPagesProperty = + AvaloniaProperty.Register(nameof(NumberOfPages)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsPreviousButtonVisibleProperty = + AvaloniaProperty.Register(nameof(IsPreviousButtonVisible), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedPageIndexProperty = + AvaloniaProperty.Register(nameof(SelectedPageIndex), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TemplateSettingsProperty = + AvaloniaProperty.RegisterDirect(nameof(TemplateSettings), + x => x.TemplateSettings); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PreviousButtonStyleProperty = + AvaloniaProperty.Register(nameof(PreviousButtonStyle)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty NextButtonStyleProperty = + AvaloniaProperty.Register(nameof(NextButtonStyle)); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent SelectedIndexChangedEvent = + RoutedEvent.Register(nameof(SelectedIndexChanged), RoutingStrategies.Bubble); + + /// + /// Occurs when the selected index has changed. + /// + public event EventHandler? SelectedIndexChanged + { + add => AddHandler(SelectedIndexChangedEvent, value); + remove => RemoveHandler(SelectedIndexChangedEvent, value); + } + + static PipsPager() + { + SelectedPageIndexProperty.Changed.AddClassHandler((x, e) => x.OnSelectedPageIndexChanged(e)); + NumberOfPagesProperty.Changed.AddClassHandler((x, e) => x.OnNumberOfPagesChanged(e)); + IsPreviousButtonVisibleProperty.Changed.AddClassHandler((x, e) => x.OnIsPreviousButtonVisibleChanged(e)); + IsNextButtonVisibleProperty.Changed.AddClassHandler((x, e) => x.OnIsNextButtonVisibleChanged(e)); + OrientationProperty.Changed.AddClassHandler((x, e) => x.OnOrientationChanged(e)); + MaxVisiblePipsProperty.Changed.AddClassHandler((x, e) => x.OnMaxVisiblePipsChanged(e)); + } + + /// + /// Initializes a new instance of . + /// + public PipsPager() + { + UpdatePseudoClasses(); + } + + /// + /// Gets or sets the maximum number of visible pips. + /// + public int MaxVisiblePips + { + get => GetValue(MaxVisiblePipsProperty); + set => SetValue(MaxVisiblePipsProperty, value); + } + + /// + /// Gets or sets the visibility of the next button. + /// + public bool IsNextButtonVisible + { + get => GetValue(IsNextButtonVisibleProperty); + set => SetValue(IsNextButtonVisibleProperty, value); + } + + /// + /// Gets or sets the number of pages. + /// + public int NumberOfPages + { + get => GetValue(NumberOfPagesProperty); + set => SetValue(NumberOfPagesProperty, value); + } + + /// + /// Gets or sets the orientation of the pips. + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets the visibility of the previous button. + /// + public bool IsPreviousButtonVisible + { + get => GetValue(IsPreviousButtonVisibleProperty); + set => SetValue(IsPreviousButtonVisibleProperty, value); + } + + /// + /// Gets or sets the current selected page index. + /// + public int SelectedPageIndex + { + get => GetValue(SelectedPageIndexProperty); + set => SetValue(SelectedPageIndexProperty, value); + } + + /// + /// Gets the template settings. + /// + public PipsPagerTemplateSettings TemplateSettings + { + get => _templateSettings; + private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value); + } + + /// + /// Gets or sets the style for the previous button. + /// + public ControlTheme? PreviousButtonStyle + { + get => GetValue(PreviousButtonStyleProperty); + set => SetValue(PreviousButtonStyleProperty, value); + } + + /// + /// Gets or sets the style for the next button. + /// + public ControlTheme? NextButtonStyle + { + get => GetValue(NextButtonStyleProperty); + set => SetValue(NextButtonStyleProperty, value); + } + + /// + protected override AutomationPeer OnCreateAutomationPeer() + { + return new PipsPagerAutomationPeer(this); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _scrollAnimationCts?.Cancel(); + _scrollAnimationCts?.Dispose(); + _scrollAnimationCts = null; + _isInitialLoad = true; + + // Unsubscribe from previous button events + if (_previousButton != null) + { + _previousButton.Click -= PreviousButton_Click; + } + + if (_nextButton != null) + { + _nextButton.Click -= NextButton_Click; + } + + // Unsubscribe from previous list events + if (_pipsPagerList != null) + { + _pipsPagerList.SizeChanged -= OnPipsPagerListSizeChanged; + _pipsPagerList.ContainerPrepared -= OnContainerPrepared; + _pipsPagerList.ContainerIndexChanged -= OnContainerIndexChanged; + } + + // Get template parts + _previousButton = e.NameScope.Find + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml b/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml new file mode 100644 index 0000000000..388fc0e4d6 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/PipsPager.xaml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 11a25dde9d..e5ba9163b5 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -40,6 +40,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs b/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs new file mode 100644 index 0000000000..ffc0469ce3 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/PipsPagerTests.cs @@ -0,0 +1,578 @@ +using Avalonia.Input; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using System.Linq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class PipsPagerTests : ScopedTestBase + { + [Fact] + public void NumberOfPages_Should_Update_Pips() + { + var target = new PipsPager(); + + target.NumberOfPages = 5; + + Assert.Equal(5, target.TemplateSettings.Pips.Count); + Assert.Equal(1, target.TemplateSettings.Pips[0]); + Assert.Equal(5, target.TemplateSettings.Pips[4]); + } + + [Fact] + public void Decreasing_NumberOfPages_Should_Update_Pips() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + + target.NumberOfPages = 3; + + Assert.Equal(3, target.TemplateSettings.Pips.Count); + } + + [Fact] + public void Decreasing_NumberOfPages_Should_Update_SelectedPageIndex() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + target.SelectedPageIndex = 4; + + target.NumberOfPages = 3; + + Assert.Equal(2, target.SelectedPageIndex); + } + + [Fact] + public void SelectedPageIndex_Should_Be_Clamped_To_Zero() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + + target.SelectedPageIndex = -1; + + Assert.Equal(0, target.SelectedPageIndex); + } + + [Fact] + public void SelectedPageIndex_Change_Should_Raise_Event() + { + var target = new PipsPager(); + target.NumberOfPages = 5; + var raised = false; + target.SelectedIndexChanged += (s, e) => raised = true; + + target.SelectedPageIndex = 2; + + Assert.True(raised); + } + + [Fact] + public void Next_Button_Should_Increment_Index() + { + using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); + + var target = new PipsPager + { + NumberOfPages = 5, + SelectedPageIndex = 1, + IsNextButtonVisible = true, + Template = GetTemplate() + }; + + var root = new TestRoot(target); + target.ApplyTemplate(); + + var nextButton = target.GetVisualDescendants().OfType
public class TabStripItem : ListBoxItem { - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); UpdateSelectionFromEvent(e); diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index e93180fff4..2593c4b475 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -766,7 +766,7 @@ namespace Avalonia.Controls } } - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index f3532763f6..493c1631d4 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -160,14 +160,14 @@ namespace Avalonia.Controls SetCurrentValue(SelectionEndProperty, SelectionStart); } - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); UpdateCommandStates(); } - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { base.OnLostFocus(e); diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 67274247c2..cb619a941d 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -237,7 +237,7 @@ namespace Avalonia.Controls public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs) { - if (eventArgs is GotFocusEventArgs { NavigationMethod: not NavigationMethod.Directional }) + if (eventArgs is FocusChangedEventArgs { NavigationMethod: not NavigationMethod.Directional }) { return false; } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 07f2cd6505..a4829c16ca 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls e.Handled = true; } - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); UpdateSelectionFromEvent(e); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 447a6a41fc..eadb54b58a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1088,7 +1088,7 @@ namespace Avalonia.Controls CanPaste = !IsReadOnly; } - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { base.OnGotFocus(e); @@ -1114,7 +1114,7 @@ namespace Avalonia.Controls _presenter?.ShowCaret(); } - protected override void OnLostFocus(RoutedEventArgs e) + protected override void OnLostFocus(FocusChangedEventArgs e) { base.OnLostFocus(e); diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 033be87c7f..d48997f390 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -550,7 +550,7 @@ namespace Avalonia.Controls } /// - protected override void OnGotFocus(GotFocusEventArgs e) + protected override void OnGotFocus(FocusChangedEventArgs e) { if (e.NavigationMethod == NavigationMethod.Directional) { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index f0f9b820f7..1c2d43e94f 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.UnitTests Prepare(target); - target.Presenter!.Panel!.Children[0].RaiseEvent(new GotFocusEventArgs + target.Presenter!.Panel!.Children[0].RaiseEvent(new FocusChangedEventArgs(InputElement.GotFocusEvent) { NavigationMethod = NavigationMethod.Tab, }); @@ -57,7 +57,7 @@ namespace Avalonia.Controls.UnitTests AvaloniaLocator.CurrentMutable.Bind().ToConstant(new PlatformHotkeyConfiguration()); Prepare(target); - target.Presenter!.Panel!.Children[0].RaiseEvent(new GotFocusEventArgs + target.Presenter!.Panel!.Children[0].RaiseEvent(new FocusChangedEventArgs(InputElement.GotFocusEvent) { NavigationMethod = NavigationMethod.Directional, KeyModifiers = KeyModifiers.Control From 7d900944555bf63bd08a9ba5721ca04c6bea59b6 Mon Sep 17 00:00:00 2001 From: Melissa Date: Fri, 13 Mar 2026 09:17:45 +0100 Subject: [PATCH 14/24] Made GetMaxSizeFromConstraint a protected method (#20877) --- src/Avalonia.Controls/TextBlock.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c70d06ae7f..5c798f6a83 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -363,7 +363,10 @@ namespace Avalonia.Controls internal bool HasComplexContent => Inlines != null && Inlines.Count > 0; - private protected Size GetMaxSizeFromConstraint() + /// + /// Gets the maximum available size based on the constraint of the control + /// + protected Size GetMaxSizeFromConstraint() { var maxWidth = double.IsNaN(_constraint.Width) ? 0.0 : _constraint.Width; var maxHeight = double.IsNaN(_constraint.Height) ? 0.0 : _constraint.Height; From 2ffb4d01e00130377d1015d3a52eb4a5978bb741 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Mar 2026 17:24:20 +0900 Subject: [PATCH 15/24] Add /update-api and /api-diff commands (#20887) * Add update-api command * Api diff command * Missed flag * Restrict commands running on fork PRs * Add concurrency * Filter github.event.comment.author_association even before workflow started * Use steps.pr.outputs.sha * Only push api/ changes --- .github/workflows/api-diff.yml | 180 +++++++++++++++++++++++++++++++ .github/workflows/update-api.yml | 123 +++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 .github/workflows/api-diff.yml create mode 100644 .github/workflows/update-api.yml diff --git a/.github/workflows/api-diff.yml b/.github/workflows/api-diff.yml new file mode 100644 index 0000000000..f855380f9e --- /dev/null +++ b/.github/workflows/api-diff.yml @@ -0,0 +1,180 @@ +name: Output API Diff + +on: + issue_comment: + types: [created] + +permissions: {} + +concurrency: + group: api-diff-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + api-diff: + name: Output API Diff + if: >- + github.event.issue.pull_request + && contains(github.event.comment.body, '/api-diff') + && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - name: Check maintainer permission + uses: actions/github-script@v7 + with: + script: | + const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.comment.user.login, + }); + const allowed = ['admin', 'maintain', 'write']; + if (!allowed.includes(permLevel.permission)) { + core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`); + } + + - name: Add reaction to acknowledge command + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes', + }); + + - name: Get PR branch info + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) { + core.setFailed('Cannot run /api-diff on fork PRs — would execute untrusted code.'); + return; + } + core.setOutput('ref', pr.head.ref); + core.setOutput('sha', pr.head.sha); + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.pr.outputs.sha }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Run OutputApiDiff + run: dotnet run --project ./nukebuild/_build.csproj -- OutputApiDiff + + - name: Post API diff as PR comment + if: always() && steps.pr.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const diffDir = path.join(process.env.GITHUB_WORKSPACE, 'artifacts', 'api-diff', 'markdown'); + const mergedPath = path.join(diffDir, '_diff.md'); + + let body; + if (fs.existsSync(mergedPath)) { + let diff = fs.readFileSync(mergedPath, 'utf8').trim(); + if (!diff || diff.toLowerCase().includes('no changes')) { + body = '### API Diff\n\n✅ No public API changes detected in this PR.'; + } else { + const MAX_COMMENT_LENGTH = 60000; // GitHub comment limit is 65536 + const header = '### API Diff\n\n'; + const footer = '\n\n---\n_Generated by `/api-diff` command._'; + const budget = MAX_COMMENT_LENGTH - header.length - footer.length; + + if (diff.length > budget) { + diff = diff.substring(0, budget) + '\n\n> ⚠️ Output truncated. See the [full workflow run](' + + `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + + ') for complete diff.'; + } + + body = header + diff + footer; + } + } else { + body = '### API Diff\n\n⚠️ No diff output was produced. Check the [workflow run](' + + `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + + ') for details.'; + } + + // Collapse into
if large + if (body.length > 2000) { + const inner = body; + body = '
\n📋 API Diff (click to expand)\n\n' + inner + '\n\n
'; + } + + // Update existing bot comment or create a new one + const marker = ''; + body = marker + '\n' + body; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + const existing = comments.find(c => c.body?.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + - name: Add success reaction + if: success() + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket', + }); + + - name: Report failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1', + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `❌ \`/api-diff\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`, + }); diff --git a/.github/workflows/update-api.yml b/.github/workflows/update-api.yml new file mode 100644 index 0000000000..611a4ead50 --- /dev/null +++ b/.github/workflows/update-api.yml @@ -0,0 +1,123 @@ +name: Update API Suppressions + +on: + issue_comment: + types: [created] + +permissions: {} + +concurrency: + group: update-api-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + update-api: + name: Update API Suppressions + if: >- + github.event.issue.pull_request + && contains(github.event.comment.body, '/update-api') + && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - name: Check maintainer permission + uses: actions/github-script@v7 + with: + script: | + const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.comment.user.login, + }); + const allowed = ['admin', 'maintain', 'write']; + if (!allowed.includes(permLevel.permission)) { + core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`); + } + + - name: Add reaction to acknowledge command + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes', + }); + + - name: Get PR branch info + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) { + core.setFailed('Cannot run /update-api on fork PRs — would execute untrusted code with write permissions.'); + return; + } + core.setOutput('ref', pr.head.ref); + core.setOutput('sha', pr.head.sha); + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.pr.outputs.sha }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Run ValidateApiDiff + run: dotnet run --project ./nukebuild/_build.csproj -- ValidateApiDiff --update-api-suppression true + + - name: Commit and push changes + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add api/ + if git diff --cached --quiet; then + echo "No API suppression changes to commit." + else + git commit -m "Update API suppressions" + git push origin HEAD:${{ steps.pr.outputs.ref }} + fi + + - name: Add success reaction + if: success() + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket', + }); + + - name: Report failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1', + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `❌ \`/update-api\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`, + }); From 7bdf54ee355eb811f6bc305fa29aa8d67a956300 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 13 Mar 2026 08:44:32 +0000 Subject: [PATCH 16/24] Remove Uncompilable code guarded by platform defines (#20861) * remove uncompilable code guarded by platform defines * Remove OperatingSystemEx * remove NativeLibraryEx * remove unused classes * fix unused usings --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 14 -- src/Avalonia.Base/Avalonia.Base.csproj | 8 +- .../CollectionCompatibilityExtensions.cs | 32 ---- .../Compatibility/NativeLibrary.cs | 122 -------------- .../Compatibility/OperatingSystem.cs | 32 ---- .../Compatibility/StringSyntaxAttribute.cs | 43 ----- .../Reflection/DynamicPluginStreamNode.cs | 2 - .../Reflection/ExpressionTreeIndexerNode.cs | 2 - .../Core/Parsers/ExpressionNodeFactory.cs | 2 - .../Data/Core/Plugins/BindingPlugins.cs | 4 - .../Plugins/ReflectionMethodAccessorPlugin.cs | 4 - src/Avalonia.Base/Input/DataFormat.cs | 14 +- src/Avalonia.Base/Input/KeyGesture.cs | 4 +- src/Avalonia.Base/Layout/LayoutHelper.cs | 5 - .../Media/Fonts/FamilyNameCollection.cs | 4 - src/Avalonia.Base/Media/GlyphRun.cs | 2 - .../TextFormatting/FormattingBufferHelper.cs | 15 -- src/Avalonia.Base/Media/Typeface.cs | 6 +- .../Platform/Internal/UnmanagedBlob.cs | 13 +- .../Platform/StandardRuntimePlatform.cs | 12 +- .../StandardRuntimePlatformServices.cs | 3 - .../Platform/Storage/FileIO/BclLauncher.cs | 11 +- .../Storage/FileIO/BclStorageProvider.cs | 7 +- .../Storage/FileIO/SecurityScopedStream.cs | 8 - .../Storage/FileIO/StorageBookmarkHelper.cs | 12 +- .../Composition/Server/FpsCounter.cs | 4 - .../Composition/Server/FrameTimeGraph.cs | 5 - .../Composition/Transport/BatchStream.cs | 5 +- .../AvaloniaSynchronizationContext.cs | 3 - .../CulturePreservingExecutionContext.cs | 156 ------------------ .../Threading/DispatcherOperation.cs | 8 - .../Threading/NonPumpingSyncContext.cs | 7 - src/Avalonia.Base/Utilities/ArrayBuilder.cs | 4 - .../Utilities/AvaloniaPropertyDictionary.cs | 10 +- src/Avalonia.Base/Utilities/EnumHelper.cs | 34 ---- src/Avalonia.Base/Utilities/Polyfills.cs | 43 ----- .../Utilities/RefCountingSmallDictionary.cs | 20 +-- .../Utilities/SmallDictionary.cs | 3 - src/Avalonia.Base/Utilities/StringSplitter.cs | 4 - .../Helpers/ColorHelper.cs | 7 +- .../TextBoxTextInputMethodClient.cs | 4 - .../Remote/HtmlTransport/HtmlTransport.cs | 5 +- .../AppBuilderDesktopExtensions.cs | 8 +- .../ManagedFileDialogExtensions.cs | 2 - src/Avalonia.Native/AvaloniaNativePlatform.cs | 6 +- src/Avalonia.Native/IAvnMenu.cs | 4 +- src/Avalonia.Native/IAvnMenuItem.cs | 6 +- src/Avalonia.OpenGL/Egl/EglInterface.cs | 11 +- src/Avalonia.OpenGL/OpenGlException.cs | 4 - .../ColorPaletteResourcesCollection.cs | 2 - src/Avalonia.X11/X11Window.cs | 5 - .../Input/EvDev/EvDevDevice.cs | 4 - .../MarkupExtensions/OnPlatformExtension.cs | 16 +- .../ReflectionBindingExtension.cs | 2 - .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 11 -- src/Markup/Avalonia.Markup/Data/Binding.cs | 4 +- src/Shared/ModuleInitializer.cs | 2 +- .../Helpers/PixelFormatHelper.cs | 3 +- src/Skia/Avalonia.Skia/SKRoundRectCache.cs | 6 - .../AutomationNode.cs | 14 -- .../Interop/IDockProvider.cs | 5 - .../Interop/IExpandCollapseProvider.cs | 5 - .../Interop/IGridItemProvider.cs | 5 - .../Interop/IGridProvider.cs | 5 - .../Interop/IInvokeProvider.cs | 5 - .../Interop/IMultipleViewProvider.cs | 8 +- .../Interop/IRangeValueProvider.cs | 5 - .../IRawElementProviderAdviseEvents.cs | 9 - .../Interop/IRawElementProviderFragment.cs | 9 - .../IRawElementProviderFragmentRoot.cs | 5 - .../Interop/IRawElementProviderSimple.cs | 7 - .../Interop/IRawElementProviderSimple2.cs | 15 -- .../Interop/IScrollItemProvider.cs | 5 - .../Interop/IScrollProvider.cs | 5 - .../Interop/ISelectionItemProvider.cs | 5 - .../Interop/ISelectionProvider.cs | 7 - .../Interop/ISynchronizedInputProvider.cs | 6 +- .../Interop/ITableItemProvider.cs | 9 - .../Interop/ITableProvider.cs | 10 +- .../Interop/ITextProvider.cs | 9 - .../Interop/ITextRangeProvider.cs | 13 -- .../Interop/IToggleProvider.cs | 5 - .../Interop/ITransformProvider.cs | 5 - .../Interop/IValueProvider.cs | 5 - .../Interop/IWindowProvider.cs | 6 +- .../Interop/UiaCoreProviderApi.cs | 27 --- .../Interop/UiaCoreTypesApi.cs | 25 --- .../InteropAutomationNode.cs | 4 - .../Marshalling/ComVariant.cs | 2 - .../Marshalling/ComVariantMarshaller.cs | 4 +- .../Marshalling/SafeArrayMarshaller.cs | 4 +- .../Marshalling/SafeArrayRef.cs | 3 - .../RootAutomationNode.cs | 8 - .../Avalonia.Win32/OleDataObjectHelper.cs | 4 - .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 17 +- .../Avalonia.Designer.HostApp.csproj | 1 - .../Avalonia.Designer.HostApp/TinyJson.cs | 4 - 97 files changed, 67 insertions(+), 1022 deletions(-) delete mode 100644 src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs delete mode 100644 src/Avalonia.Base/Compatibility/NativeLibrary.cs delete mode 100644 src/Avalonia.Base/Compatibility/OperatingSystem.cs delete mode 100644 src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs delete mode 100644 src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs delete mode 100644 src/Avalonia.Base/Utilities/EnumHelper.cs delete mode 100644 src/Avalonia.Base/Utilities/Polyfills.cs diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 892e320afc..adcf844552 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -228,13 +228,8 @@ namespace ControlCatalog.Pages try { // Sync disposal of StreamWriter is not supported on WASM -#if NET6_0_OR_GREATER await using var stream = await file.OpenWriteAsync(); await using var writer = new System.IO.StreamWriter(stream); -#else - using var stream = await file.OpenWriteAsync(); - using var writer = new System.IO.StreamWriter(stream); -#endif await writer.WriteLineAsync(openedFileContent.Text); SetFolder(await file.GetParentAsync()); @@ -265,13 +260,8 @@ namespace ControlCatalog.Pages if (result.File is { } file) { // Sync disposal of StreamWriter is not supported on WASM -#if NET6_0_OR_GREATER await using var stream = await file.OpenWriteAsync(); await using var writer = new System.IO.StreamWriter(stream); -#else - using var stream = await file.OpenWriteAsync(); - using var writer = new System.IO.StreamWriter(stream); -#endif if (result.SelectedFileType == FilePickerFileTypes.Xml) { await writer.WriteLineAsync("Test"); @@ -431,11 +421,7 @@ namespace ControlCatalog.Pages internal static async Task ReadTextFromFile(IStorageFile file, int length) { -#if NET6_0_OR_GREATER await using var stream = await file.OpenReadAsync(); -#else - using var stream = await file.OpenReadAsync(); -#endif using var reader = new System.IO.StreamReader(stream); // 4GB file test, shouldn't load more than 10000 chars into a memory. diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index ffaac716b9..99524857a7 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -16,11 +16,7 @@ - - - - @@ -65,8 +61,6 @@ - + diff --git a/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs b/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs deleted file mode 100644 index e22288a74d..0000000000 --- a/src/Avalonia.Base/Compatibility/CollectionCompatibilityExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace System; - -#if !NET6_0_OR_GREATER -internal static class CollectionCompatibilityExtensions -{ - public static bool Remove( - this Dictionary o, - TKey key, - [MaybeNullWhen(false)] out TValue value) - where TKey : notnull - { - if (o.TryGetValue(key, out value)) - return o.Remove(key); - return false; - } - - public static bool TryAdd(this Dictionary o, TKey key, TValue value) - where TKey : notnull - { - if (!o.ContainsKey(key)) - { - o.Add(key, value); - return true; - } - - return false; - } -} -#endif diff --git a/src/Avalonia.Base/Compatibility/NativeLibrary.cs b/src/Avalonia.Base/Compatibility/NativeLibrary.cs deleted file mode 100644 index 7627c095bc..0000000000 --- a/src/Avalonia.Base/Compatibility/NativeLibrary.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.ComponentModel; -using System.Reflection; -using System.Runtime.InteropServices; -using Avalonia.Compatibility; -using Avalonia.Platform.Interop; - -namespace Avalonia.Compatibility -{ - internal class NativeLibraryEx - { -#if NET6_0_OR_GREATER - public static IntPtr Load(string dll, Assembly assembly) => NativeLibrary.Load(dll, assembly, null); - public static IntPtr Load(string dll) => NativeLibrary.Load(dll); - public static bool TryGetExport(IntPtr handle, string name, out IntPtr address) => - NativeLibrary.TryGetExport(handle, name, out address); -#else - public static IntPtr Load(string dll, Assembly assembly) => Load(dll); - public static IntPtr Load(string dll) - { - var handle = DlOpen!(dll); - if (handle != IntPtr.Zero) - return handle; - throw new InvalidOperationException("Unable to load " + dll, DlError!()); - } - - public static bool TryGetExport(IntPtr handle, string name, out IntPtr address) - { - try - { - address = DlSym!(handle, name); - return address != default; - } - catch (Exception) - { - address = default; - return false; - } - } - - static NativeLibraryEx() - { - if (OperatingSystemEx.IsWindows()) - { - Win32Imports.Init(); - } - else if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS()) - { - var buffer = Marshal.AllocHGlobal(0x1000); - uname(buffer); - var unixName = Marshal.PtrToStringAnsi(buffer); - Marshal.FreeHGlobal(buffer); - if (unixName == "Darwin") - OsXImports.Init(); - else - LinuxImports.Init(); - } - } - - private static Func? DlOpen; - private static Func? DlSym; - private static Func? DlError; - - [DllImport("libc")] - static extern int uname(IntPtr buf); - - static class Win32Imports - { - [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] - private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); - - [DllImport("kernel32", EntryPoint = "LoadLibraryW", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern IntPtr LoadLibrary(string lpszLib); - - public static void Init() - { - DlOpen = LoadLibrary; - DlSym = GetProcAddress; - DlError = () => new Win32Exception(Marshal.GetLastWin32Error()); - } - } - - static class LinuxImports - { - [DllImport("libdl.so.2")] - private static extern IntPtr dlopen(string path, int flags); - - [DllImport("libdl.so.2")] - private static extern IntPtr dlsym(IntPtr handle, string symbol); - - [DllImport("libdl.so.2")] - private static extern IntPtr dlerror(); - - public static void Init() - { - DlOpen = s => dlopen(s, 1); - DlSym = dlsym; - DlError = () => new InvalidOperationException(Marshal.PtrToStringAnsi(dlerror())); - } - } - - static class OsXImports - { - [DllImport("/usr/lib/libSystem.dylib")] - private static extern IntPtr dlopen(string path, int flags); - - [DllImport("/usr/lib/libSystem.dylib")] - private static extern IntPtr dlsym(IntPtr handle, string symbol); - - [DllImport("/usr/lib/libSystem.dylib")] - private static extern IntPtr dlerror(); - - public static void Init() - { - DlOpen = s => dlopen(s, 1); - DlSym = dlsym; - DlError = () => new InvalidOperationException(Marshal.PtrToStringAnsi(dlerror())); - } - } -#endif - } -} diff --git a/src/Avalonia.Base/Compatibility/OperatingSystem.cs b/src/Avalonia.Base/Compatibility/OperatingSystem.cs deleted file mode 100644 index ad5fe0246a..0000000000 --- a/src/Avalonia.Base/Compatibility/OperatingSystem.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Avalonia.Compatibility -{ - internal sealed class OperatingSystemEx - { -#if NET6_0_OR_GREATER - public static bool IsWindows() => OperatingSystem.IsWindows(); - public static bool IsMacOS() => OperatingSystem.IsMacOS(); - public static bool IsMacCatalyst() => OperatingSystem.IsMacCatalyst(); - public static bool IsLinux() => OperatingSystem.IsLinux(); - public static bool IsFreeBSD() => OperatingSystem.IsFreeBSD(); - public static bool IsAndroid() => OperatingSystem.IsAndroid(); - public static bool IsIOS() => OperatingSystem.IsIOS(); - public static bool IsTvOS() => OperatingSystem.IsTvOS(); - public static bool IsBrowser() => OperatingSystem.IsBrowser(); - public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform); -#else - public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - public static bool IsFreeBSD() => false; - public static bool IsAndroid() => false; - public static bool IsIOS() => false; - public static bool IsMacCatalyst() => false; - public static bool IsTvOS() => false; - public static bool IsBrowser() => false; - public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); -#endif - } -} diff --git a/src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs b/src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs deleted file mode 100644 index 2b3585fbe4..0000000000 --- a/src/Avalonia.Base/Compatibility/StringSyntaxAttribute.cs +++ /dev/null @@ -1,43 +0,0 @@ -#pragma warning disable MA0048 // File name must match type name -// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -// ReSharper disable once CheckNamespace -namespace System.Diagnostics.CodeAnalysis -{ -#if !NET7_0_OR_GREATER - /// Specifies the syntax used in a string. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - internal sealed class StringSyntaxAttribute : Attribute - { - /// Initializes the with the identifier of the syntax used. - /// The syntax identifier. - public StringSyntaxAttribute(string syntax) - { - Syntax = syntax; - Arguments = Array.Empty(); - } - - /// Initializes the with the identifier of the syntax used. - /// The syntax identifier. - /// Optional arguments associated with the specific syntax employed. - public StringSyntaxAttribute(string syntax, params object?[] arguments) - { - Syntax = syntax; - Arguments = arguments; - } - - /// Gets the identifier of the syntax used. - public string Syntax { get; } - - /// Optional arguments associated with the specific syntax employed. - public object?[] Arguments { get; } - - /// The syntax identifier for strings containing XML. - public const string Xml = nameof(Xml); - } -#endif -} diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs index dd8c0e1a63..198819f1ac 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs @@ -7,9 +7,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core.ExpressionNodes.Reflection; [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] -#if NET8_0_OR_GREATER [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] -#endif internal sealed class DynamicPluginStreamNode : ExpressionNode { private IDisposable? _subscription; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs index dfb83fb10d..ef8aa34752 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs @@ -16,9 +16,7 @@ internal sealed class ExpressionTreeIndexerNode : CollectionNodeBase, ISettableN private readonly Delegate _getDelegate; private readonly Delegate _firstArgumentDelegate; -#if NET8_0_OR_GREATER [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] -#endif public ExpressionTreeIndexerNode(IndexExpression expression) { var valueParameter = Expression.Parameter(expression.Type); diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs index f7eb2d537d..301e8c0796 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionNodeFactory.cs @@ -15,9 +15,7 @@ namespace Avalonia.Data.Core.Parsers internal static class ExpressionNodeFactory { [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] -#if NET8_0_OR_GREATER [RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] -#endif public static List? CreateFromAst( List astNodes, Func? typeResolver, diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs index 50e137eac1..516dbecc26 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs @@ -32,11 +32,7 @@ namespace Avalonia.Data.Core.Plugins { // When building with AOT, don't create ReflectionMethodAccessorPlugin instance. // This branch can be eliminated in compile time with AOT. -#if NET6_0_OR_GREATER if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) -#else - if (true) -#endif { s_propertyAccessors.Insert(1, new ReflectionMethodAccessorPlugin()); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs index f9a4587ca6..d2e6f23e29 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs @@ -7,9 +7,7 @@ using System.Reflection; namespace Avalonia.Data.Core.Plugins { [RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] -#if NET8_0_OR_GREATER [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] -#endif internal class ReflectionMethodAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup = @@ -84,9 +82,7 @@ namespace Avalonia.Data.Core.Plugins return found; } -#if NET8_0_OR_GREATER [RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] -#endif private sealed class Accessor : PropertyAccessorBase { public Accessor(WeakReference reference, MethodInfo method) diff --git a/src/Avalonia.Base/Input/DataFormat.cs b/src/Avalonia.Base/Input/DataFormat.cs index 7e35bab411..14d1d4a30b 100644 --- a/src/Avalonia.Base/Input/DataFormat.cs +++ b/src/Avalonia.Base/Input/DataFormat.cs @@ -213,19 +213,7 @@ public abstract class DataFormat : IEquatable return true; static bool IsValidChar(char c) - => IsAsciiLetterOrDigit(c) || c == '.' || c == '-'; - - static bool IsAsciiLetterOrDigit(char c) - { -#if NET8_0_OR_GREATER - return char.IsAsciiLetterOrDigit(c); -#else - return c is - (>= '0' and <= '9') or - (>= 'A' and <= 'Z') or - (>= 'a' and <= 'z'); -#endif - } + => char.IsAsciiLetterOrDigit(c) || c == '.' || c == '-'; } /// diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 83d99bf7a9..463337ddda 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -167,7 +167,7 @@ namespace Avalonia.Input if (s_keySynonyms.TryGetValue(keyStr.ToLower(CultureInfo.InvariantCulture), out key)) return true; - if (EnumHelper.TryParse(keyStr, true, out key)) + if (Enum.TryParse(keyStr, true, out key)) return true; return false; @@ -187,7 +187,7 @@ namespace Avalonia.Input return KeyModifiers.Meta; } - return EnumHelper.Parse(modifier.ToString(), true); + return Enum.Parse(modifier.ToString(), true); } private static Key ResolveNumPadOperationKey(Key key) diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index c50053dc05..fd81ffaa49 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -263,12 +263,7 @@ namespace Avalonia.Layout // point precision error (e.g. 79.333333333333343) then when it's multiplied by // `dpiScale` and rounded up, it will be rounded up to a value one greater than it // should be. -#if NET6_0_OR_GREATER return Math.Round(value, 8, MidpointRounding.ToZero); -#else - // MidpointRounding.ToZero isn't available in netstandard2.0. - return Math.Truncate(value * 1e8) / 1e8; -#endif } } } diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index dabe935b76..ed916bb441 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs @@ -43,11 +43,7 @@ namespace Avalonia.Media.Fonts } private static string[] SplitNames(string names) -#if NET6_0_OR_GREATER => names.Split(',', StringSplitOptions.TrimEntries); -#else - => Array.ConvertAll(names.Split(','), p => p.Trim()); -#endif /// /// Gets the primary family name. diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 41eed5b747..cccef8f938 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -126,12 +126,10 @@ namespace Avalonia.Media return array.AsSpan(); } -#if NET6_0_OR_GREATER if (list is List concreteList) { return CollectionsMarshal.AsSpan(concreteList); } -#endif array = new ushort[count]; for (var i = 0; i < count; ++i) diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs index c27903cd55..d8672ffba8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -52,11 +52,7 @@ namespace Avalonia.Media.TextFormatting // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here if (IsBufferTooLarge>(approximateCapacity)) { -#if NET6_0_OR_GREATER dictionary.TrimExcess(); -#else - dictionary = new Dictionary(); -#endif } } @@ -67,18 +63,7 @@ namespace Avalonia.Media.TextFormatting [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint RoundUpToPowerOf2(uint value) { -#if NET6_0_OR_GREATER return BitOperations.RoundUpToPowerOf2(value); -#else - // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 - --value; - value |= value >> 1; - value |= value >> 2; - value |= value >> 4; - value |= value >> 8; - value |= value >> 16; - return value + 1; -#endif } } } diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 1adcac5b75..f745ba2d23 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -174,17 +174,17 @@ namespace Avalonia.Media // Try match with font style, weight or stretch and update accordingly. var match = false; - if (EnumHelper.TryParse(token, true, out var newStyle)) + if (Enum.TryParse(token, true, out var newStyle)) { style = newStyle; match = true; } - else if (EnumHelper.TryParse(token, true, out var newWeight)) + else if (Enum.TryParse(token, true, out var newWeight)) { weight = newWeight; match = true; } - else if (EnumHelper.TryParse(token, true, out var newStretch)) + else if (Enum.TryParse(token, true, out var newStretch)) { stretch = newStretch; match = true; diff --git a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs b/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs index eeba160a3c..a1296c2ee1 100644 --- a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs +++ b/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; -using Avalonia.Compatibility; namespace Avalonia.Platform.Internal; @@ -117,7 +116,7 @@ internal class UnmanagedBlob : IDisposable // Could be replaced with https://github.com/dotnet/runtime/issues/40892 when it will be available. private IntPtr Alloc(int size) { - if (!OperatingSystemEx.IsLinux()) + if (!OperatingSystem.IsLinux()) { return Marshal.AllocHGlobal(size); } @@ -126,12 +125,8 @@ internal class UnmanagedBlob : IDisposable var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero); if (rv.ToInt64() == -1 || (ulong)rv.ToInt64() == 0xffffffff) { -#if NET6_0_OR_GREATER var errno = Marshal.GetLastSystemError(); throw new Exception("Unable to allocate memory: " + errno); -#else - throw new Exception("Unable to allocate memory"); -#endif } return rv; } @@ -139,7 +134,7 @@ internal class UnmanagedBlob : IDisposable private void Free(IntPtr ptr, int len) { - if (!OperatingSystemEx.IsLinux()) + if (!OperatingSystem.IsLinux()) { Marshal.FreeHGlobal(ptr); } @@ -147,12 +142,8 @@ internal class UnmanagedBlob : IDisposable { if (munmap(ptr, new IntPtr(len)) == -1) { -#if NET6_0_OR_GREATER var errno = Marshal.GetLastSystemError(); throw new Exception("Unable to free memory: " + errno); -#else - throw new Exception("Unable to free memory"); -#endif } } } diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs index b72e10c831..fc44cbbbd7 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs @@ -1,4 +1,4 @@ -using Avalonia.Compatibility; +using System; using Avalonia.Metadata; namespace Avalonia.Platform @@ -8,11 +8,11 @@ namespace Avalonia.Platform { public virtual RuntimePlatformInfo GetRuntimeInfo() => new() { - IsDesktop = OperatingSystemEx.IsWindows() - || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsMacCatalyst() - || OperatingSystemEx.IsLinux() || OperatingSystemEx.IsFreeBSD(), - IsMobile = OperatingSystemEx.IsAndroid() || (OperatingSystemEx.IsIOS() && !OperatingSystemEx.IsMacCatalyst()), - IsTV = OperatingSystemEx.IsTvOS() + IsDesktop = OperatingSystem.IsWindows() + || OperatingSystem.IsMacOS() || OperatingSystem.IsMacCatalyst() + || OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD(), + IsMobile = OperatingSystem.IsAndroid() || (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()), + IsTV = OperatingSystem.IsTvOS() }; } } diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs index 70919bc477..666cdf2bed 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs @@ -1,7 +1,4 @@ using System.Reflection; -using Avalonia.Compatibility; -using Avalonia.Platform.Internal; -using Avalonia.Platform.Interop; namespace Avalonia.Platform; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs index 96f489a222..da4cb01df5 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Avalonia.Compatibility; namespace Avalonia.Platform.Storage.FileIO; @@ -39,7 +38,7 @@ internal class BclLauncher : ILauncher private static bool Exec(string urlOrFile) { - if (OperatingSystemEx.IsLinux()) + if (OperatingSystem.IsLinux()) { // If no associated application/json MimeType is found xdg-open opens return error // but it tries to open it anyway using the console editor (nano, vim, other..) @@ -47,17 +46,17 @@ internal class BclLauncher : ILauncher ShellExecRaw($"xdg-open \\\"{args}\\\"", waitForExit: false); return true; } - else if (OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS()) + else if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()) { var info = new ProcessStartInfo { - FileName = OperatingSystemEx.IsWindows() ? urlOrFile : "open", + FileName = OperatingSystem.IsWindows() ? urlOrFile : "open", CreateNoWindow = true, - UseShellExecute = OperatingSystemEx.IsWindows() + UseShellExecute = OperatingSystem.IsWindows() }; // Using the argument list avoids having to escape spaces and other special // characters that are part of valid macos file and folder paths. - if (OperatingSystemEx.IsMacOS()) + if (OperatingSystem.IsMacOS()) info.ArgumentList.Add(urlOrFile); using var process = Process.Start(info); return true; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index decb742ed8..a471dba720 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Avalonia.Compatibility; using Avalonia.Logging; namespace Avalonia.Platform.Storage.FileIO; @@ -107,13 +106,13 @@ internal abstract class BclStorageProvider : IStorageProvider // Normally we want to avoid platform specific code in the Avalonia.Base assembly. protected static string? GetDownloadsWellKnownFolder() { - if (OperatingSystemEx.IsWindows()) + if (OperatingSystem.IsWindows()) { return Environment.OSVersion.Version.Major < 6 ? null : TryGetWindowsKnownFolder(s_folderDownloads); } - if (OperatingSystemEx.IsLinux()) + if (OperatingSystem.IsLinux()) { var envDir = Environment.GetEnvironmentVariable("XDG_DOWNLOAD_DIR"); if (envDir != null && Directory.Exists(envDir)) @@ -122,7 +121,7 @@ internal abstract class BclStorageProvider : IStorageProvider } } - if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS()) + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { return "~/Downloads"; } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs index 0e0ffa3b1b..ad2bd9f37a 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs @@ -40,12 +40,10 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _stream.ReadAsync(buffer, offset, count, cancellationToken); -#if NET6_0_OR_GREATER public override int Read(Span buffer) => _stream.Read(buffer); public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => _stream.ReadAsync(buffer, cancellationToken); -#endif public override void Write(byte[] buffer, int offset, int count) => _stream.Write(buffer, offset, count); @@ -53,12 +51,10 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _stream.WriteAsync(buffer, offset, count, cancellationToken); -#if NET6_0_OR_GREATER public override void Write(ReadOnlySpan buffer) => _stream.Write(buffer); public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => _stream.WriteAsync(buffer, cancellationToken); -#endif public override void WriteByte(byte value) => _stream.WriteByte(value); @@ -68,9 +64,7 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu public override void SetLength(long value) => _stream.SetLength(value); -#if NET6_0_OR_GREATER public override void CopyTo(Stream destination, int bufferSize) => _stream.CopyTo(destination, bufferSize); -#endif public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _stream.CopyToAsync(destination, bufferSize, cancellationToken); @@ -100,7 +94,6 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu } } -#if NET6_0_OR_GREATER public override async ValueTask DisposeAsync() { try @@ -112,5 +105,4 @@ internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _secu _securityScope.Dispose(); } } -#endif } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs index 78392ec31d..4c43331803 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs @@ -58,11 +58,7 @@ internal static class StorageBookmarkHelper nativeBookmarkBytes.CopyTo(arraySpan.Slice(HeaderLength)); // We must use span overload because ArrayPool might return way too big array. -#if NET6_0_OR_GREATER return Convert.ToBase64String(arraySpan); -#else - return Convert.ToBase64String(arraySpan.ToArray(), Base64FormattingOptions.None); -#endif } finally { @@ -89,7 +85,7 @@ internal static class StorageBookmarkHelper } Span decodedBookmark; -#if NET6_0_OR_GREATER + // Each base64 character represents 6 bits, but to be safe, var arrayPool = ArrayPool.Shared.Rent(HeaderLength + base64bookmark.Length * 6); if (Convert.TryFromBase64Chars(base64bookmark, arrayPool, out int bytesWritten)) @@ -101,9 +97,7 @@ internal static class StorageBookmarkHelper nativeBookmark = null; return DecodeResult.InvalidFormat; } -#else - decodedBookmark = Convert.FromBase64String(base64bookmark).AsSpan(); -#endif + try { if (decodedBookmark.Length < HeaderLength @@ -126,9 +120,7 @@ internal static class StorageBookmarkHelper } finally { -#if NET6_0_OR_GREATER ArrayPool.Shared.Return(arrayPool); -#endif } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index c13e0d04ae..81f41e3a42 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -42,11 +42,7 @@ internal class FpsCounter _lastFpsUpdate = now; } -#if NET6_0_OR_GREATER var fpsLine = string.Create(CultureInfo.InvariantCulture, $"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}"); -#else - var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}"); -#endif var size = _textRenderer.MeasureAsciiText(fpsLine.AsSpan()); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs index 8e283ba5b1..c5672a1860 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs @@ -104,14 +104,9 @@ internal sealed class FrameTimeGraph var brush = value <= _defaultMaxY ? Brushes.Black : Brushes.Red; -#if NET6_0_OR_GREATER Span buffer = stackalloc char[24]; buffer.TryWrite(CultureInfo.InvariantCulture, $"{label}: {value,5:F2}ms", out var charsWritten); _textRenderer.DrawAsciiText(context, buffer.Slice(0, charsWritten), brush); -#else - var text = FormattableString.Invariant($"{label}: {value,5:F2}ms"); - _textRenderer.DrawAsciiText(context, text.AsSpan(), brush); -#endif } private IStreamGeometryImpl BuildGraphGeometry(double maxY) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index 0231c29bb3..1fcadae1c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -33,11 +33,8 @@ static unsafe class UnalignedMemoryHelper { public static T ReadUnaligned(byte* src) where T : unmanaged { -#if NET6_0_OR_GREATER Unsafe.SkipInit(out var rv); -#else - T rv; -#endif + UnalignedMemcpy((byte*)&rv, src, Unsafe.SizeOf()); return rv; } diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index a643445e99..5639963e9e 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -74,9 +74,6 @@ namespace Avalonia.Threading _dispatcher.Send(d, state, Priority); } -#if !NET6_0_OR_GREATER - [PrePrepareMethod] -#endif public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) { if ( diff --git a/src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs b/src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs deleted file mode 100644 index ec0ebaa4a6..0000000000 --- a/src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs +++ /dev/null @@ -1,156 +0,0 @@ -#if NET6_0_OR_GREATER -// In .NET Core, the security context and call context are not supported, however, -// the impersonation context and culture would typically flow with the execution context. -// See: https://learn.microsoft.com/en-us/dotnet/api/system.threading.executioncontext -// -// So we can safely use ExecutionContext without worrying about culture flowing issues. -#else -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Threading; - -namespace Avalonia.Threading; - -/// -/// An ExecutionContext that preserves culture information across async operations. -/// This is a modernized version that removes legacy compatibility switches and -/// includes nullable reference type annotations. -/// -internal sealed class CulturePreservingExecutionContext -{ - private readonly ExecutionContext _context; - private CultureAndContext? _cultureAndContext; - - private CulturePreservingExecutionContext(ExecutionContext context) - { - _context = context; - } - - /// - /// Captures the current ExecutionContext and culture information. - /// - /// A new CulturePreservingExecutionContext instance, or null if no context needs to be captured. - public static CulturePreservingExecutionContext? Capture() - { - // ExecutionContext.SuppressFlow had been called. - // We expect ExecutionContext.Capture() to return null, so match that behavior and return null. - if (ExecutionContext.IsFlowSuppressed()) - { - return null; - } - - var context = ExecutionContext.Capture(); - if (context == null) - return null; - - return new CulturePreservingExecutionContext(context); - } - - /// - /// Runs the specified callback in the captured execution context while preserving culture information. - /// This method is used for .NET Framework and earlier .NET versions. - /// - /// The execution context to run in. - /// The callback to execute. - /// The state to pass to the callback. - public static void Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, object? state) - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (callback == null) - return; - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (executionContext == null) - ThrowNullContext(); - - // Save culture information - we will need this to restore just before - // the callback is actually invoked from CallbackWrapper. - executionContext._cultureAndContext = CultureAndContext.Initialize(callback, state); - - try - { - ExecutionContext.Run( - executionContext._context, - s_callbackWrapperDelegate, - executionContext._cultureAndContext); - } - finally - { - // Restore culture information - it might have been modified during callback execution. - executionContext._cultureAndContext.RestoreCultureInfos(); - } - } - - [DoesNotReturn] - private static void ThrowNullContext() - { - throw new InvalidOperationException("ExecutionContext cannot be null."); - } - - private static readonly ContextCallback s_callbackWrapperDelegate = CallbackWrapper; - - /// - /// Executes the callback and saves culture values immediately afterwards. - /// - /// Contains the actual callback and state. - private static void CallbackWrapper(object? obj) - { - var cultureAndContext = (CultureAndContext)obj!; - - // Restore culture information saved during Run() - cultureAndContext.RestoreCultureInfos(); - - try - { - // Execute the actual callback - cultureAndContext.Callback(cultureAndContext.State); - } - finally - { - // Save any culture changes that might have occurred during callback execution - cultureAndContext.CaptureCultureInfos(); - } - } - - /// - /// Helper class to manage culture information across execution contexts. - /// - private sealed class CultureAndContext - { - public ContextCallback Callback { get; } - public object? State { get; } - - private CultureInfo? _culture; - private CultureInfo? _uiCulture; - - private CultureAndContext(ContextCallback callback, object? state) - { - Callback = callback; - State = state; - CaptureCultureInfos(); - } - - public static CultureAndContext Initialize(ContextCallback callback, object? state) - { - return new CultureAndContext(callback, state); - } - - public void CaptureCultureInfos() - { - _culture = Thread.CurrentThread.CurrentCulture; - _uiCulture = Thread.CurrentThread.CurrentUICulture; - } - - public void RestoreCultureInfos() - { - if (_culture != null) - Thread.CurrentThread.CurrentCulture = _culture; - - if (_uiCulture != null) - Thread.CurrentThread.CurrentUICulture = _uiCulture; - } - } -} -#endif diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index 3a4513652e..ea48fa31b0 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -5,11 +5,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -#if NET6_0_OR_GREATER using ExecutionContext = System.Threading.ExecutionContext; -#else -using ExecutionContext = Avalonia.Threading.CulturePreservingExecutionContext; -#endif namespace Avalonia.Threading; @@ -277,12 +273,8 @@ public class DispatcherOperation { if (_executionContext is { } executionContext) { -#if NET6_0_OR_GREATER ExecutionContext.Restore(executionContext); InvokeCore(); -#else - ExecutionContext.Run(executionContext, static s => ((DispatcherOperation)s!).InvokeCore(), this); -#endif } else { diff --git a/src/Avalonia.Base/Threading/NonPumpingSyncContext.cs b/src/Avalonia.Base/Threading/NonPumpingSyncContext.cs index 03fc0cc76c..3c12d693eb 100644 --- a/src/Avalonia.Base/Threading/NonPumpingSyncContext.cs +++ b/src/Avalonia.Base/Threading/NonPumpingSyncContext.cs @@ -22,11 +22,7 @@ namespace Avalonia.Threading { if (_inner is null) { -#if NET6_0_OR_GREATER ThreadPool.QueueUserWorkItem(static x => x.d(x.state), (d, state), false); -#else - ThreadPool.QueueUserWorkItem(_ => d(state)); -#endif } else { @@ -46,9 +42,6 @@ namespace Avalonia.Threading } } -#if !NET6_0_OR_GREATER - [PrePrepareMethod] -#endif public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) => _impl.Wait(waitHandles, waitAll, millisecondsTimeout); diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index bbbcc39ecc..c12a4906f3 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -136,7 +136,6 @@ namespace Avalonia.Utilities /// public void Clear() { -#if NET6_0_OR_GREATER if (RuntimeHelpers.IsReferenceOrContainsReferences()) { ClearArray(); @@ -145,9 +144,6 @@ namespace Avalonia.Utilities { _size = 0; } -#else - ClearArray(); -#endif } private void ClearArray() diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs index ab34e85220..13272c7e7d 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs @@ -244,12 +244,8 @@ namespace Avalonia.Utilities // hi and lo are never negative: there's no overflow using unsigned math var i = (int)(((uint)hi + (uint)lo) >> 1); -#if NET6_0_OR_GREATER // nuint cast to force zero extend instead of sign extend ref var entry = ref Unsafe.Add(ref entry0, (nuint)i); -#else - ref var entry = ref Unsafe.Add(ref entry0, i); -#endif var entryId = entry.Id; if (entryId == propertyId) @@ -288,12 +284,8 @@ namespace Avalonia.Utilities // hi and lo are never negative: there's no overflow using unsigned math var i = (int)(((uint)hi + (uint)lo) >> 1); -#if NET6_0_OR_GREATER // nuint cast to force zero extend instead of sign extend ref var entry = ref Unsafe.Add(ref entry0, (nuint)i); -#else - ref var entry = ref Unsafe.Add(ref entry0, i); -#endif var entryId = entry.Id; if (entryId == propertyId) @@ -360,7 +352,7 @@ namespace Avalonia.Utilities [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref Entry UnsafeGetEntryRef(int index) { -#if NET6_0_OR_GREATER && !DEBUG +#if !DEBUG // This type is performance critical: in release mode, skip any bound check the JIT compiler couldn't elide. // The index parameter should always be correct when calling this method: no unchecked user input should get here. return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries!), (uint)index); diff --git a/src/Avalonia.Base/Utilities/EnumHelper.cs b/src/Avalonia.Base/Utilities/EnumHelper.cs deleted file mode 100644 index fd9176985e..0000000000 --- a/src/Avalonia.Base/Utilities/EnumHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace Avalonia.Utilities -{ - internal class EnumHelper - { -#if NET6_0_OR_GREATER - public static T Parse(ReadOnlySpan key, bool ignoreCase) where T : struct - { - return Enum.Parse(key, ignoreCase); - } - - public static bool TryParse(ReadOnlySpan key, bool ignoreCase, out T result) where T : struct - { - return Enum.TryParse(key, ignoreCase, out result); - } -#else - public static T Parse(string key, bool ignoreCase) where T : struct - { - return (T)Enum.Parse(typeof(T), key, ignoreCase); - } - - public static bool TryParse(string key, bool ignoreCase, out T result) where T : struct - { - return Enum.TryParse(key, ignoreCase, out result); - } - - public static bool TryParse(ReadOnlySpan key, bool ignoreCase, out T result) where T : struct - { - return Enum.TryParse(key.ToString(), ignoreCase, out result); - } -#endif - } -} diff --git a/src/Avalonia.Base/Utilities/Polyfills.cs b/src/Avalonia.Base/Utilities/Polyfills.cs deleted file mode 100644 index 9ee72ab112..0000000000 --- a/src/Avalonia.Base/Utilities/Polyfills.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -internal static class Polyfills -{ - #if !NET6_0_OR_GREATER - - public static bool TryDequeue(this Queue queue, [MaybeNullWhen(false)]out T item) - { - if (queue.Count == 0) - { - item = default; - return false; - } - - item = queue.Dequeue(); - return true; - } - - #endif -} - -#if !NET7_0_OR_GREATER - -namespace System.Diagnostics.CodeAnalysis -{ - [System.AttributeUsage( - System.AttributeTargets.Method | System.AttributeTargets.Parameter | System.AttributeTargets.Property, - AllowMultiple = false, Inherited = false)] - internal sealed class UnscopedRefAttribute : Attribute - { - } - - struct S - { - int _field; - - // Okay: `field` has the ref-safe-to-escape of `this` which is *calling method* because - // it is a `ref` - [UnscopedRef] ref int Prop1 => ref _field; - } -} -#endif diff --git a/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs b/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs index 86c9fd7ba1..64838a845f 100644 --- a/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs @@ -10,20 +10,13 @@ internal struct RefCountingSmallDictionary : IEnumerable : IEnumerable : IEnumerable> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index bfb3a32bac..99283522af 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -177,7 +177,6 @@ internal struct InlineDictionary : IEnumerable : IEnumerable _cachedKnownColorNames = new Dictionary(); private static readonly object _displayNameCacheMutex = new object(); private static readonly object _knownColorCacheMutex = new object(); - private static readonly KnownColor[] _knownColors = -#if NET6_0_OR_GREATER - Enum.GetValues(); -#else - (KnownColor[])Enum.GetValues(typeof(KnownColor)); -#endif + private static readonly KnownColor[] _knownColors = Enum.GetValues(); /// /// Gets the relative (perceptual) luminance/brightness of the given color. diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 12e8e97640..c61a220990 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -181,11 +181,7 @@ namespace Avalonia.Controls { if (run.Length > 0) { -#if NET6_0_OR_GREATER builder.Append(run.Text.Span); -#else - builder.Append(run.Text.Span.ToArray()); -#endif } } diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 869e682940..a27f532292 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Viewport; -using Avalonia.Utilities; using InputProtocol = Avalonia.Remote.Protocol.Input; namespace Avalonia.DesignerSupport.Remote.HtmlTransport @@ -354,13 +353,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport ? null : modifiersText .Split(',') - .Select(x => EnumHelper.Parse(x, true)) + .Select(x => Enum.Parse(x, true)) .ToArray(); private static InputProtocol.MouseButton ParseMouseButton(string buttonText) => string.IsNullOrWhiteSpace(buttonText) ? InputProtocol.MouseButton.None - : EnumHelper.Parse(buttonText, true); + : Enum.Parse(buttonText, true); private static double ParseDouble(string text) => double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs b/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs index 6c85ca40fc..9fd370e0dd 100644 --- a/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs +++ b/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs @@ -1,4 +1,4 @@ -using Avalonia.Compatibility; +using System; using Avalonia.Logging; namespace Avalonia @@ -17,17 +17,17 @@ namespace Avalonia // Additionally, by having a hard reference to each assembly, // we verify that the assemblies are in the final .deps.json file // so .NET Core knows where to load the assemblies from. - if (OperatingSystemEx.IsWindows()) + if (OperatingSystem.IsWindows()) { LoadWin32(builder); LoadSkia(builder); } - else if (OperatingSystemEx.IsMacOS()) + else if (OperatingSystem.IsMacOS()) { LoadAvaloniaNative(builder); LoadSkia(builder); } - else if (OperatingSystemEx.IsLinux()) + else if (OperatingSystem.IsLinux()) { LoadX11(builder); LoadSkia(builder); diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index c8f7d388f5..16995f91a8 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -6,9 +6,7 @@ using Avalonia.Platform.Storage; namespace Avalonia.Dialogs { -#if NET6_0_OR_GREATER [SupportedOSPlatform("windows"), SupportedOSPlatform("macos"), SupportedOSPlatform("linux")] -#endif public static class ManagedFileDialogExtensions { internal class ManagedStorageProviderFactory : IStorageProviderFactory diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 40bc2ca71e..34f0ee766e 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; -using Avalonia.Compatibility; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -47,8 +45,8 @@ namespace Avalonia.Native { if (options.AvaloniaNativeLibraryPath != null) { - var lib = NativeLibraryEx.Load(options.AvaloniaNativeLibraryPath); - if (!NativeLibraryEx.TryGetExport(lib, "CreateAvaloniaNative", out var proc)) + var lib = NativeLibrary.Load(options.AvaloniaNativeLibraryPath); + if (!NativeLibrary.TryGetExport(lib, "CreateAvaloniaNative", out var proc)) { throw new InvalidOperationException( "Unable to get \"CreateAvaloniaNative\" export from AvaloniaNativeLibrary library"); diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index b526d91924..05588eb57c 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using Avalonia.Compatibility; -using Avalonia.Reactive; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -51,7 +49,7 @@ namespace Avalonia.Native.Interop.Impl private void UpdateTitle(string? title) { - if (OperatingSystemEx.IsMacOS()) + if (OperatingSystem.IsMacOS()) { // macOS does not process access key markers, so remove them. title = AccessText.RemoveAccessKeyMarker(title); diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index f9b3e2d2d6..441b974df2 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -1,11 +1,9 @@ using System; using System.IO; -using Avalonia.Compatibility; -using Avalonia.Reactive; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Media.Imaging; -using Avalonia.Platform.Interop; +using Avalonia.Reactive; namespace Avalonia.Native.Interop { @@ -26,7 +24,7 @@ namespace Avalonia.Native.Interop.Impl private void UpdateTitle(string? title) { - if (OperatingSystemEx.IsMacOS()) + if (OperatingSystem.IsMacOS()) { // macOS does not process access key markers, so remove them. title = AccessText.RemoveAccessKeyMarker(title); diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index 5c93d2084b..492c646da2 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -1,8 +1,5 @@ using System; using System.Runtime.InteropServices; -using Avalonia.Compatibility; -using Avalonia.Platform; -using Avalonia.Platform.Interop; using Avalonia.SourceGenerator; namespace Avalonia.OpenGL.Egl @@ -25,9 +22,9 @@ namespace Avalonia.OpenGL.Egl static Func Load() { - if(OperatingSystemEx.IsLinux()) + if(OperatingSystem.IsLinux()) return Load("libEGL.so.1"); - if (OperatingSystemEx.IsAndroid()) + if (OperatingSystem.IsAndroid()) return Load("libEGL.so"); throw new PlatformNotSupportedException(); @@ -35,8 +32,8 @@ namespace Avalonia.OpenGL.Egl static Func Load(string library) { - var lib = NativeLibraryEx.Load(library); - return (s) => NativeLibraryEx.TryGetExport(lib, s, out var address) ? address : default; + var lib = NativeLibrary.Load(library); + return (s) => NativeLibrary.TryGetExport(lib, s, out var address) ? address : default; } // ReSharper disable UnassignedGetOnlyAutoProperty diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index d7a42c4400..616fe0f187 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -37,11 +37,7 @@ namespace Avalonia.OpenGL { try { -#if NET6_0_OR_GREATER var errorName = Enum.GetName(errorCode); -#else - var errorName = Enum.GetName(typeof(T), errorCode); -#endif return new OpenGlException( $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", intErrorCode); } diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs index 828cb2e899..3b2173e133 100644 --- a/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs @@ -130,9 +130,7 @@ internal sealed class ColorPaletteResourcesCollection : ResourceProvider, IDicti } bool IDictionary.TryGetValue(ThemeVariant key, -#if NET6_0_OR_GREATER [MaybeNullWhen(false)] -#endif out ColorPaletteResources value) { return _inner.TryGetValue(key, out value); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 52456ca1b6..a57e1986ac 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -395,12 +395,7 @@ namespace Avalonia.X11 private static int GetProcessId() { -#if NET6_0_OR_GREATER var pid = Environment.ProcessId; -#else - using var currentProcess = Process.GetCurrentProcess(); - var pid = currentProcess.Id; -#endif return pid; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs index 92d8bbf268..85f190a848 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs @@ -18,11 +18,7 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev Fd = fd; _dev = dev; Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev)); -#if NET6_0_OR_GREATER foreach (EvType type in Enum.GetValues()) -#else - foreach (EvType type in Enum.GetValues(typeof(EvType))) -#endif { if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0) EventTypes.Add(type); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs index e393fbc7cf..0b1ae4638f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs @@ -1,4 +1,4 @@ -using Avalonia.Compatibility; +using System; using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.MarkupExtensions; @@ -76,13 +76,13 @@ public abstract class OnPlatformExtensionBase : IAddChild // IsOSPlatform might work better with trimming in the future, so it should be re-visited after .NET 8/9. return option switch { - "WINDOWS" => OperatingSystemEx.IsWindows(), - "OSX" => OperatingSystemEx.IsMacOS(), - "LINUX" => OperatingSystemEx.IsLinux(), - "ANDROID" => OperatingSystemEx.IsAndroid(), - "IOS" => OperatingSystemEx.IsIOS(), - "BROWSER" => OperatingSystemEx.IsBrowser(), - _ => OperatingSystemEx.IsOSPlatform(option) + "WINDOWS" => OperatingSystem.IsWindows(), + "OSX" => OperatingSystem.IsMacOS(), + "LINUX" => OperatingSystem.IsLinux(), + "ANDROID" => OperatingSystem.IsAndroid(), + "IOS" => OperatingSystem.IsIOS(), + "BROWSER" => OperatingSystem.IsBrowser(), + _ => OperatingSystem.IsOSPlatform(option) }; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs index f4b7864185..71d0902c0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs @@ -6,9 +6,7 @@ using Avalonia.Data; namespace Avalonia.Markup.Xaml.MarkupExtensions { [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] -#if NET8_0_OR_GREATER [RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] -#endif public sealed class ReflectionBindingExtension : ReflectionBinding { /// diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index f98a1cc60b..d2f8fe437e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -129,22 +129,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime return false; } -#if NET6_0_OR_GREATER if (!CollectionsMarshal.AsSpan(resourceNodes).SequenceEqual(lastResourceNodes)) { cachedResourceNodes = null; return false; } -#else - for (var i = 0; i < lastResourceNodes.Length; ++i) - { - if (lastResourceNodes[i] != resourceNodes[i]) - { - cachedResourceNodes = null; - return false; - } - } -#endif cachedResourceNodes = lastResourceNodes; return true; diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 4e624b6479..931c17fd0e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -8,9 +8,7 @@ namespace Avalonia.Data; /// for new code. /// [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] -#if NET8_0_OR_GREATER - [RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] -#endif +[RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] public class Binding : ReflectionBinding { public Binding() { } diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs index e58b296474..d65fd7ffa4 100644 --- a/src/Shared/ModuleInitializer.cs +++ b/src/Shared/ModuleInitializer.cs @@ -2,7 +2,7 @@ namespace System.Runtime.CompilerServices { #if NETSTANDARD2_0 [AttributeUsage(AttributeTargets.Method)] - internal sealed class ModuleInitializerAttribute : Attribute + internal sealed class ModuleInitializerhAttribute : Attribute { } diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs index f1cd39b2a7..21c9f3cdf9 100644 --- a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -1,5 +1,4 @@ -using Avalonia.Compatibility; -using Avalonia.Platform; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia.Helpers diff --git a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs index b84c61303d..7b041fb9f1 100644 --- a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs +++ b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs @@ -91,13 +91,7 @@ namespace Avalonia.Skia base.Clear(); // Clear out the cache of SKPoint arrays. -#if NET6_0_OR_GREATER _radiiCache.Clear(); -#else - while (_radiiCache.TryTake(out var item)) - { - } -#endif } } } diff --git a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs index bcf3e782b4..2ae4294dcb 100644 --- a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs @@ -18,15 +18,8 @@ using UIA = Avalonia.Win32.Automation.Interop; namespace Avalonia.Win32.Automation { -#if NET8_0_OR_GREATER [GeneratedComClass] internal partial class AutomationNode : -#else -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Requires .NET COM interop")] -#endif - internal partial class AutomationNode : MarshalByRefObject, -#endif IRawElementProviderSimple, IRawElementProviderSimple2, IRawElementProviderFragment, @@ -202,9 +195,7 @@ namespace Avalonia.Win32.Automation public void SetFocus() => InvokeSync(() => Peer.SetFocus()); -#if NET6_0_OR_GREATER [return: NotNullIfNotNull(nameof(peer))] -#endif public static AutomationNode? GetOrCreate(AutomationPeer? peer) { return peer is null ? null : s_nodes.GetValue(peer, Create); @@ -434,12 +425,7 @@ namespace Avalonia.Win32.Automation private static int GetProcessId() { -#if NET6_0_OR_GREATER return Environment.ProcessId; -#else - using var proccess = Process.GetCurrentProcess(); - return proccess.Id; -#endif } } } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs index c65e76366a..80509e8aa7 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IDockProvider.cs @@ -14,12 +14,7 @@ internal enum DockPosition None } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("159bc72c-4ad3-485e-9637-d7052edf0146")] internal partial interface IDockProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs index ee04a24ce7..7e5cc048de 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IExpandCollapseProvider.cs @@ -4,12 +4,7 @@ using Avalonia.Automation; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("d847d3a5-cab0-4a98-8c32-ecb45c59ad24")] internal partial interface IExpandCollapseProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs index f764427417..541f656a34 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IGridItemProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("d02541f1-fb81-4d64-ae32-f520f8a6dbd1")] internal partial interface IGridItemProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs index cfc295fa7d..b3a7cfd47a 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IGridProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("b17d6187-0907-464b-a168-0ef17a1572b1")] internal partial interface IGridProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs index 7737a1bb74..1ef0cac481 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IInvokeProvider.cs @@ -7,12 +7,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("54fcb24b-e18e-47a2-b4d3-eccbe77599a2")] internal partial interface IInvokeProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs index dcd0d35e74..c21790acab 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IMultipleViewProvider.cs @@ -5,12 +5,8 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif + [Guid("6278cab1-b556-4a1a-b4e0-418acc523201")] internal partial interface IMultipleViewProvider { @@ -18,8 +14,6 @@ internal partial interface IMultipleViewProvider string GetViewName(int viewId); void SetCurrentView(int viewId); int GetCurrentView(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[] GetSupportedViews(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs index a8f921fa26..089b1d65ad 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRangeValueProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("36dc7aef-33e6-4691-afe1-2be7274b3d33")] internal partial interface IRangeValueProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs index 9d2e16ab94..6f73b73790 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderAdviseEvents.cs @@ -5,24 +5,15 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("a407b27b-0f6d-4427-9292-473c7bf93258")] internal partial interface IRawElementProviderAdviseEvents { void AdviseEventAdded(int eventId, -#if NET8_0_OR_GREATER [MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[] properties); void AdviseEventRemoved(int eventId, -#if NET8_0_OR_GREATER [MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[] properties); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs index 0bb56c8b68..7550ecf0df 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragment.cs @@ -15,24 +15,15 @@ internal enum NavigateDirection LastChild, } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("f7063da8-8359-439c-9297-bbc5299a7d87")] internal partial interface IRawElementProviderFragment { IRawElementProviderFragment? Navigate(NavigateDirection direction); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif int[]? GetRuntimeId(); Rect GetBoundingRectangle(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[]? GetEmbeddedFragmentRoots(); void SetFocus(); IRawElementProviderFragmentRoot? GetFragmentRoot(); diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs index 349e58b7b3..430559665a 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderFragmentRoot.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("620ce2a5-ab8f-40a9-86cb-de3c75599b58")] internal partial interface IRawElementProviderFragmentRoot { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs index bf70aa1f40..6f2233d7f6 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs @@ -307,21 +307,14 @@ internal enum UiaLiveSetting Assertive, }; -#if NET8_0_OR_GREATER [GeneratedComInterface] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("d6dd68d1-86fd-4332-8666-9abedea2d24c")] internal partial interface IRawElementProviderSimple { ProviderOptions GetProviderOptions(); [return: MarshalAs(UnmanagedType.Interface)] object? GetPatternProvider(int patternId); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(ComVariantMarshaller))] -#endif object? GetPropertyValue(int propertyId); IRawElementProviderSimple? GetHostRawElementProvider(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs index 7bd48f4e78..ed62c621d4 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple2.cs @@ -3,24 +3,9 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("a0a839a9-8da1-4a82-806a-8e0d44e79f56")] internal partial interface IRawElementProviderSimple2 : IRawElementProviderSimple { -#if !NET8_0_OR_GREATER - // Hack for the legacy COM interop - // See https://learn.microsoft.com/en-us/dotnet/standard/native-interop/comwrappers-source-generation#derived-interfaces - new ProviderOptions GetProviderOptions(); - [return: MarshalAs(UnmanagedType.Interface)] - new object? GetPatternProvider(int patternId); - [return: MarshalAs(UnmanagedType.Struct)] - new object? GetPropertyValue(int propertyId); - new IRawElementProviderSimple? GetHostRawElementProvider(); -#endif void ShowContextMenu(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs index 8e022c988d..20d5690de3 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollItemProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("2360c714-4bf1-4b26-ba65-9b21316127eb")] internal partial interface IScrollItemProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs index 1113685592..bda05f540f 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IScrollProvider.cs @@ -4,12 +4,7 @@ using Avalonia.Automation.Provider; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("b38b8077-1fc3-42a5-8cae-d40c2215055a")] internal partial interface IScrollProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs index a4f4d56e54..d36cb605a5 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionItemProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("2acad808-b2d4-452d-a407-91ff1ad167b2")] internal partial interface ISelectionItemProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs index 2a30c97f18..3db1e3a4a4 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ISelectionProvider.cs @@ -5,18 +5,11 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("fb8b03af-3bdf-48d4-bd36-1a65793be168")] internal partial interface ISelectionProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetSelection(); [return: MarshalAs(UnmanagedType.Bool)] diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs index 75850461c3..24f8948f12 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ISynchronizedInputProvider.cs @@ -13,12 +13,8 @@ internal enum SynchronizedInputType MouseRightButtonUp = 0x10, MouseRightButtonDown = 0x20 } -#if NET8_0_OR_GREATER + [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("29db1a06-02ce-4cf7-9b42-565d4fab20ee")] internal partial interface ISynchronizedInputProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs index 75bdf48bb8..33b9d30062 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITableItemProvider.cs @@ -5,21 +5,12 @@ using Avalonia.Win32.Automation.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("b9734fa6-771f-4d78-9c90-2517999349cd")] internal partial interface ITableItemProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetRowHeaderItems(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetColumnHeaderItems(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs index 6acacbdf5d..5f4bc589d6 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITableProvider.cs @@ -12,22 +12,14 @@ internal enum RowOrColumnMajor ColumnMajor, Indeterminate, } -#if NET8_0_OR_GREATER + [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("9c860395-97b3-490a-b52a-858cc22af166")] internal partial interface ITableProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetRowHeaders(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetColumnHeaders(); RowOrColumnMajor GetRowOrColumnMajor(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs index 63a92ce547..d3c94ecd07 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITextProvider.cs @@ -15,22 +15,13 @@ internal enum SupportedTextSelection Multiple, } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("3589c92c-63f3-4367-99bb-ada653b77cf2")] internal partial interface ITextProvider { -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif ITextRangeProvider[] GetSelection(); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif ITextRangeProvider[] GetVisibleRanges(); ITextRangeProvider RangeFromChild(IRawElementProviderSimple childElement); diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs index 18f167a87a..72f43ef8b2 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITextRangeProvider.cs @@ -22,12 +22,7 @@ internal enum TextUnit Document = 6, } -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("5347ad7b-c355-46f8-aff5-909033582f63")] internal partial interface ITextRangeProvider { @@ -42,22 +37,16 @@ internal partial interface ITextRangeProvider void ExpandToEnclosingUnit(TextUnit unit); ITextRangeProvider FindAttribute(int attribute, -#if NET8_0_OR_GREATER [MarshalUsing(typeof(ComVariantMarshaller))] -#endif object value, [MarshalAs(UnmanagedType.Bool)] bool backward); ITextRangeProvider FindText( [MarshalAs(UnmanagedType.BStr)] string text, [MarshalAs(UnmanagedType.Bool)] bool backward, [MarshalAs(UnmanagedType.Bool)] bool ignoreCase); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(ComVariantMarshaller))] -#endif object GetAttributeValue(int attribute); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif double[] GetBoundingRectangles(); IRawElementProviderSimple GetEnclosingElement(); [return: MarshalAs(UnmanagedType.BStr)] @@ -72,8 +61,6 @@ internal partial interface ITextRangeProvider void AddToSelection(); void RemoveFromSelection(); void ScrollIntoView([MarshalAs(UnmanagedType.Bool)] bool alignToTop); -#if NET8_0_OR_GREATER [return: MarshalUsing(typeof(SafeArrayMarshaller))] -#endif IRawElementProviderSimple[] GetChildren(); } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs index 85dd3c0f97..157accebee 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IToggleProvider.cs @@ -4,12 +4,7 @@ using Avalonia.Automation.Provider; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("56d00bd0-c4f4-433c-a836-1a52a57e0892")] internal partial interface IToggleProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs index baabaf3664..947a850028 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/ITransformProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("6829ddc4-4f91-4ffa-b86f-bd3e2987cb4c")] internal partial interface ITransformProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs index 6d7526c054..b86a33e4cf 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IValueProvider.cs @@ -3,12 +3,7 @@ using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Interop; -#if NET8_0_OR_GREATER [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("c7935180-6fb3-4201-b174-7df73adbf64a")] internal partial interface IValueProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs index 65cec9a1f5..7f7a096aac 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/IWindowProvider.cs @@ -20,12 +20,8 @@ internal enum WindowInteractionState BlockedByModalWindow, NotResponding } -#if NET8_0_OR_GREATER + [GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)] -#else -[ComImport()] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -#endif [Guid("987df77b-db06-4d77-8f8a-86a9c3bb90b9")] internal partial interface IWindowProvider { diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs index 36e6902846..eb2384fb5b 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreProviderApi.cs @@ -66,7 +66,6 @@ namespace Avalonia.Win32.Automation.Interop { public const int UIA_E_ELEMENTNOTENABLED = unchecked((int)0x80040200); -#if NET7_0_OR_GREATER [LibraryImport("UIAutomationCore.dll", StringMarshalling = StringMarshalling.Utf8)] [return: MarshalAs(UnmanagedType.Bool)] public static partial bool UiaClientsAreListening(); @@ -88,31 +87,5 @@ namespace Avalonia.Win32.Automation.Interop [LibraryImport("UIAutomationCore.dll", StringMarshalling = StringMarshalling.Utf8)] public static partial int UiaDisconnectProvider(IRawElementProviderSimple? provider); -#else - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern bool UiaClientsAreListening(); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr UiaReturnRawElementProvider(IntPtr hwnd, IntPtr wParam, IntPtr lParam, - IRawElementProviderSimple? el); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaHostProviderFromHwnd(IntPtr hwnd, - [MarshalAs(UnmanagedType.Interface)] out IRawElementProviderSimple provider); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseAutomationEvent(IRawElementProviderSimple? provider, int id); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseAutomationPropertyChangedEvent(IRawElementProviderSimple? provider, int id, - object? oldValue, object? newValue); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseStructureChangedEvent(IRawElementProviderSimple? provider, - StructureChangeType structureChangeType, int[]? runtimeId, int runtimeIdLen); - - [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaDisconnectProvider(IRawElementProviderSimple? provider); -#endif } } diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs index a8a83dfd57..d9cc51843e 100644 --- a/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs +++ b/src/Windows/Avalonia.Win32.Automation/Interop/UiaCoreTypesApi.cs @@ -24,37 +24,12 @@ namespace Avalonia.Win32.Automation.Interop internal const int UIA_E_NOCLICKABLEPOINT = unchecked((int)0x80040202); internal const int UIA_E_PROXYASSEMBLYNOTLOADED = unchecked((int)0x80040203); - internal static bool IsNetComInteropAvailable - { - get - { -#if NET8_0_OR_GREATER - return true; -#else -#if NET6_0_OR_GREATER - if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) - { - return false; - } -#endif - var comConfig = - AppContext.GetData("System.Runtime.InteropServices.BuiltInComInterop.IsSupported") as string; - return comConfig == null || bool.Parse(comConfig); -#endif - } - } - internal static int UiaLookupId(AutomationIdType type, ref Guid guid) { return RawUiaLookupId(type, ref guid); } -#if NET7_0_OR_GREATER [LibraryImport("UIAutomationCore.dll", EntryPoint = "UiaLookupId", StringMarshalling = StringMarshalling.Utf8)] private static partial int RawUiaLookupId(AutomationIdType type, ref Guid guid); -#else - [DllImport("UIAutomationCore.dll", EntryPoint = "UiaLookupId", CharSet = CharSet.Unicode)] - private static extern int RawUiaLookupId(AutomationIdType type, ref Guid guid); -#endif } } diff --git a/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs index d6f698513c..d97561ba11 100644 --- a/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/InteropAutomationNode.cs @@ -10,11 +10,7 @@ namespace Avalonia.Win32.Automation; /// /// An automation node which serves as the root of an embedded native control automation tree. /// -#if NET8_0_OR_GREATER [GeneratedComClass] -#elif NET6_0_OR_GREATER - [RequiresUnreferencedCode("Requires .NET COM interop")] -#endif internal partial class InteropAutomationNode : AutomationNode, IRawElementProviderFragmentRoot { private readonly IntPtr _handle; diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs index a7fcd6776c..0fa78f300e 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; namespace Avalonia.Win32.Automation.Marshalling; -#if NET7_0_OR_GREATER // Oversimplified ComVariant implementation based on https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/ComVariant.cs // Available [StructLayout(LayoutKind.Explicit)] @@ -306,4 +305,3 @@ internal struct ComVariant : IDisposable private set => _typeUnion._vt = (ushort)value; } } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs index 02ae8eca28..f525e91951 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariantMarshaller.cs @@ -1,5 +1,4 @@ -#if NET7_0_OR_GREATER -global using ComVariantMarshaller = Avalonia.Win32.Automation.Marshalling.ComVariantMarshaller; +global using ComVariantMarshaller = Avalonia.Win32.Automation.Marshalling.ComVariantMarshaller; using System.Runtime.InteropServices.Marshalling; namespace Avalonia.Win32.Automation.Marshalling; @@ -13,4 +12,3 @@ internal static class ComVariantMarshaller public static void Free(ComVariant unmanaged) => unmanaged.Dispose(); } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs index fe7fe54976..ef3af4a3b2 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayMarshaller.cs @@ -1,5 +1,4 @@ -#if NET7_0_OR_GREATER -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices.Marshalling; @@ -18,4 +17,3 @@ internal static class SafeArrayMarshaller where T : notnull public static void Free(SafeArrayRef unmanaged) => unmanaged.Destroy(); } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs index 14158c4ee3..f27c67e36e 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/SafeArrayRef.cs @@ -8,13 +8,11 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Avalonia.Controls.Documents; // ReSharper disable InconsistentNaming namespace Avalonia.Win32.Automation.Marshalling; #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value -#if NET7_0_OR_GREATER internal unsafe partial struct SafeArrayRef { private SAFEARRAY* _ptr; @@ -340,4 +338,3 @@ internal unsafe partial struct SafeArrayRef } } } -#endif diff --git a/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs index 3fd59b502e..9cd4ae4850 100644 --- a/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/RootAutomationNode.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using Avalonia.Automation.Peers; @@ -9,15 +8,8 @@ using Avalonia.Win32.Automation.Interop; namespace Avalonia.Win32.Automation { -#if NET8_0_OR_GREATER [GeneratedComClass] internal partial class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot -#else -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Requires .NET COM interop")] -#endif - internal partial class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot -#endif { public RootAutomationNode(AutomationPeer peer) : base(peer) diff --git a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs index 6104613761..198c6fca07 100644 --- a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs +++ b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs @@ -674,11 +674,7 @@ internal static class OleDataObjectHelper { var data = StringBuilderCache.GetStringAndRelease(buffer); var destSpan = new Span((void*)ptr, requiredSize); -#if NET8_0_OR_GREATER MemoryMarshal.Write(destSpan, in dropFiles); -#else - MemoryMarshal.Write(destSpan, ref dropFiles); -#endif fixed (char* sourcePtr = data) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index ab86830b73..5295e2c03a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Avalonia.Automation.Peers; using Avalonia.Controls; @@ -155,10 +153,7 @@ namespace Avalonia.Win32 // The first and foremost thing to do - notify the TopLevel Closed?.Invoke(); - if (UiaCoreTypesApi.IsNetComInteropAvailable) - { - UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); - } + UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); // We need to release IMM context and state to avoid leaks. if (Imm32InputMethod.Current.Hwnd == _hwnd) @@ -224,7 +219,7 @@ namespace Avalonia.Win32 } var requestIcon = (Icons)wParam; - var requestDpi = (uint) lParam; + var requestDpi = (uint)lParam; if (requestDpi == 0) { @@ -937,7 +932,7 @@ namespace Avalonia.Win32 return IntPtr.Zero; } case WindowsMessage.WM_GETOBJECT: - if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner?.FocusRoot is Control control) + if ((long)lParam == uiaRootObjectId && _owner?.FocusRoot is Control control) { var peer = ControlAutomationPeer.CreatePeerForElement(control); var node = AutomationNode.GetOrCreate(peer); @@ -946,7 +941,7 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_WINDOWPOSCHANGED: var winPos = Marshal.PtrToStructure(lParam); - if((winPos.flags & (uint)SetWindowPosFlags.SWP_SHOWWINDOW) != 0) + if ((winPos.flags & (uint)SetWindowPosFlags.SWP_SHOWWINDOW) != 0) { OnShowHideMessage(true); } @@ -973,7 +968,7 @@ namespace Avalonia.Win32 if (message == WindowsMessage.WM_KEYDOWN) { - if(e is RawKeyEventArgs args && args.Key == Key.ImeProcessed) + if (e is RawKeyEventArgs args && args.Key == Key.ImeProcessed) { _ignoreWmChar = true; } @@ -1118,7 +1113,7 @@ namespace Avalonia.Win32 var x = mp.x > 32767 ? mp.x - 65536 : mp.x; var y = mp.y > 32767 ? mp.y - 65536 : mp.y; - if(mp.time <= prevMovePoint.time || mp.time >= movePoint.time) + if (mp.time <= prevMovePoint.time || mp.time >= movePoint.time) continue; s_sortedPoints.Add(new InternalPoint diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index e4b1d0baf7..f70c46b8c2 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -14,7 +14,6 @@ - diff --git a/src/tools/Avalonia.Designer.HostApp/TinyJson.cs b/src/tools/Avalonia.Designer.HostApp/TinyJson.cs index d48475887c..8c8b8282d7 100644 --- a/src/tools/Avalonia.Designer.HostApp/TinyJson.cs +++ b/src/tools/Avalonia.Designer.HostApp/TinyJson.cs @@ -344,11 +344,7 @@ namespace TinyJson static object ParseObject(Type type, string json) { -#if NET6_0_OR_GREATER object instance = RuntimeHelpers.GetUninitializedObject(type); -#else - object instance = FormatterServices.GetUninitializedObject(type); -#endif //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON List elems = Split(json); From a94621fd799fa863b2b8d385fdb27a19f4dd506e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Mar 2026 23:16:27 +0900 Subject: [PATCH 17/24] Use Numerge package (#20892) * Remove numerge submodule * Use numerge nuget package --- .gitmodules | 3 --- Directory.Packages.props | 1 + external/Numerge | 1 - nukebuild/_build.csproj | 6 +----- 4 files changed, 2 insertions(+), 9 deletions(-) delete mode 160000 external/Numerge diff --git a/.gitmodules b/.gitmodules index d1463ad26b..2d648aa7ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "Numerge"] - path = external/Numerge - url = https://github.com/kekekeks/Numerge.git [submodule "XamlX"] path = external/XamlX url = https://github.com/kekekeks/XamlX.git diff --git a/Directory.Packages.props b/Directory.Packages.props index 58650e1729..9441c68cac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/external/Numerge b/external/Numerge deleted file mode 160000 index 5530e1cbe9..0000000000 --- a/external/Numerge +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5530e1cbe9e105ff4ebc9da1f4af3253a8756754 diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 92f71351c4..c4d20ad4b7 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -12,6 +12,7 @@ + @@ -24,11 +25,6 @@ - - - From ae6a085ebcd002f8f4f3a47973dc6217e32261c3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 13 Mar 2026 19:27:47 +0500 Subject: [PATCH 18/24] Don't tick with render loop when app is idle (#20873) * Don't tick with render loop when app is idle * Update src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * wip * wip * api diff * fixes * Address review: clear wakeupPending at tick start, guard CarbonEmissionsHack subscriptions - Clear _wakeupPending at start of TimerTick so wakeups already processed by the current tick don't force an unnecessary extra tick - Guard CarbonEmissionsHack against duplicate subscriptions using a private attached property Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix lock-order inversion in Add/Remove vs TimerTick Move Wakeup() and Stop() calls outside the _items lock in Add/Remove to prevent deadlock with TimerTick which acquires _timerLock then _items. Add/Remove are UI-thread-only so the extracted logic remains safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: remove unused usings, guard double Stop(), fix SleepLoop extra frame - Remove unused using directives from IRenderLoopTask.cs - Guard TimerTick Stop() with _running check to prevent double Stop() when Remove() already stopped the timer - SleepLoopRenderTimer: use WaitOne(timeout) instead of Thread.Sleep so Stop() can interrupt the sleep, and recheck _stopped before Tick Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DisplayLinkTimer foreground handler bypassing render loop state Only resume the display link on WillEnterForeground if the timer was calling Start() to avoid setting _stopped=false when the render loop had the timer stopped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DisplayLinkTimer thread safety, revert global NU5104 suppression - Stop() now only sets _stopped flag; OnLinkTick() self-pauses the CADisplayLink from the timer thread to avoid thread-affinity issues - Revert NU5104 global suppression in SharedVersion.props Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cap _ticksSinceLastCommit to prevent int overflow Stop incrementing once it reaches CommitGraceTicks to prevent wrapping negative and keeping the render loop awake indefinitely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: remove Start/Stop from IRenderTimer, merge into Tick setter Timer start/stop is now controlled entirely by setting the Tick property: non-null starts, null stops. This eliminates the explicit Start()/Stop() methods from IRenderTimer, making the API simpler. DefaultRenderLoop controls the timer purely through Tick assignment under its _timerLock. A new _hasItems flag tracks subscriber presence since Tick is now transient (null when idle). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address review comments on timer thread safety and guards - ChoreographerTimer: add _frameCallbackActive guard to prevent double PostFrameCallback from both Tick setter and SubscribeView - ServerCompositor: cap _ticksSinceLastCommit at int.MaxValue - SleepLoopRenderTimer: make _tick volatile, remove _stopped recheck (guard moved to DefaultRenderLoop) - DefaultRenderLoop: add _running check at tick start to drop late ticks - ThreadProxyRenderTimer: add lock for internal state manipulation - DisplayLinkTimer: add lock for all internal state manipulation - Re-add NU5104 suppression to SharedVersion.props Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make _hasItems volatile for cross-thread visibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: guard against redundant starts in DefaultRenderTimer, make _tick volatile across all timers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove CarbonEmissionsHack, revert iOS/Android timers to always-ticking - Delete CarbonEmissionsHack class and its XAML reference - Revert DisplayLinkTimer (iOS) to original always-ticking implementation - Revert ChoreographerTimer (Android) to original always-ticking implementation - Add TODO comments for future start/stop on RenderLoop request Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DirectCompositionConnection WaitOne not respecting process exit cancellation Use WaitHandle.WaitAny with both _wakeEvent and cts.Token.WaitHandle so the loop can exit when ProcessExit fires while the timer is stopped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * timers * XML docs * Cache delegate --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/Avalonia.Headless.XUnit.nupkg.xml | 4 +- api/Avalonia.Headless.nupkg.xml | 4 +- api/Avalonia.Win32.Interoperability.nupkg.xml | 4 +- api/Avalonia.nupkg.xml | 148 +++++++++++++++++- build/SharedVersion.props | 2 +- .../Pages/ClipboardPage.xaml.cs | 5 +- .../Avalonia.Android/AndroidPlatform.cs | 4 +- src/Android/Avalonia.Android/AvaloniaView.cs | 2 +- .../Avalonia.Android/ChoreographerTimer.cs | 60 +++---- .../Rendering/Composition/Compositor.cs | 2 +- .../Server/ServerCompositionTarget.cs | 14 ++ .../Composition/Server/ServerCompositor.cs | 44 ++++-- .../Server/ServerCompositorAnimations.cs | 2 + .../Transport/BatchStreamArrayPool.cs | 75 ++++++--- .../Rendering/DefaultRenderTimer.cs | 44 ++---- src/Avalonia.Base/Rendering/IRenderLoop.cs | 16 +- .../Rendering/IRenderLoopTask.cs | 5 +- src/Avalonia.Base/Rendering/IRenderTimer.cs | 13 +- src/Avalonia.Base/Rendering/RenderLoop.cs | 140 ++++++++++++----- .../Rendering/SleepLoopRenderTimer.cs | 61 ++++---- .../Rendering/ThreadProxyRenderTimer.cs | 65 +++++--- .../Remote/PreviewerWindowingPlatform.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 9 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- .../AvaloniaNativeRenderTimer.cs | 48 +++--- src/Avalonia.X11/X11Platform.cs | 2 +- .../Rendering/BrowserRenderTimer.cs | 13 +- .../Rendering/BrowserSharedRenderLoop.cs | 2 +- .../AvaloniaHeadlessPlatform.cs | 6 +- .../LinuxFramebufferPlatform.cs | 2 +- .../DirectCompositionConnection.cs | 30 +++- .../Avalonia.Win32/DirectX/DxgiConnection.cs | 31 +++- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../Composition/WinUiCompositorConnection.cs | 30 +++- src/iOS/Avalonia.iOS/DisplayLinkTimer.cs | 12 +- src/iOS/Avalonia.iOS/Platform.cs | 2 +- .../Composition/CompositionAnimationTests.cs | 2 +- .../Compositor/CompositionTargetUpdate.cs | 6 +- .../Composition/DirectFbCompositionTests.cs | 2 +- .../Avalonia.RenderTests/ManualRenderTimer.cs | 2 +- .../Avalonia.RenderTests/TestRenderHelper.cs | 2 +- .../CompositorTestServices.cs | 7 +- tests/Avalonia.UnitTests/RendererMocks.cs | 2 +- 43 files changed, 632 insertions(+), 298 deletions(-) diff --git a/api/Avalonia.Headless.XUnit.nupkg.xml b/api/Avalonia.Headless.XUnit.nupkg.xml index c87cf909fe..15a56561b9 100644 --- a/api/Avalonia.Headless.XUnit.nupkg.xml +++ b/api/Avalonia.Headless.XUnit.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -73,4 +73,4 @@ baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll - + \ No newline at end of file diff --git a/api/Avalonia.Headless.nupkg.xml b/api/Avalonia.Headless.nupkg.xml index 229047057a..435df92d13 100644 --- a/api/Avalonia.Headless.nupkg.xml +++ b/api/Avalonia.Headless.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll - + \ No newline at end of file diff --git a/api/Avalonia.Win32.Interoperability.nupkg.xml b/api/Avalonia.Win32.Interoperability.nupkg.xml index 3672bb9b99..33fc2ac062 100644 --- a/api/Avalonia.Win32.Interoperability.nupkg.xml +++ b/api/Avalonia.Win32.Interoperability.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll - + \ No newline at end of file diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index b4c8bba386..5fe2e48f60 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -1597,6 +1597,42 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Start + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Stop + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) @@ -1609,6 +1645,30 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) @@ -3031,6 +3091,42 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Start + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Stop + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) @@ -3043,6 +3139,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) @@ -3979,6 +4099,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Start + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Stop + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 P:Avalonia.Input.IInputRoot.FocusRoot @@ -4291,6 +4423,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Start + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Stop + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 P:Avalonia.Input.IInputRoot.FocusRoot @@ -5077,4 +5221,4 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - + \ No newline at end of file diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 37d14a5647..b8c0dd4d43 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -8,7 +8,7 @@ https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link https://github.com/AvaloniaUI/Avalonia/ true - $(NoWarn);CS1591 + $(NoWarn);CS1591;NU5104 MIT Icon.png Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly. diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs index 2b87ceb7b1..4cdde6b824 100644 --- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs @@ -34,7 +34,10 @@ namespace ControlCatalog.Pages { InitializeComponent(); _clipboardLastDataObjectChecker = - new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject); + new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject) + { + IsEnabled = false + }; using var asset = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg")); _defaultImage = new Bitmap(asset); diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 7a3059cb65..460aaec5ca 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -76,19 +76,21 @@ namespace Avalonia.Android public static AndroidPlatformOptions? Options { get; private set; } internal static Compositor? Compositor { get; private set; } + internal static ChoreographerTimer? Timer { get; private set; } public static void Initialize() { Options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl()); + Timer = new ChoreographerTimer(); AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(new ChoreographerTimer()) + .Bind().ToConstant(RenderLoop.FromTimer(Timer)) .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToConstant(new AndroidActivatableLifetime()); diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 86b96772ce..d8df486bb3 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -100,7 +100,7 @@ namespace Avalonia.Android return; if (isVisible && _timerSubscription == null) { - if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer) + if (AndroidPlatform.Timer is { } timer) { _timerSubscription = timer.SubscribeView(this); } diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index adca9c72ce..9bc8e78a52 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -18,10 +18,9 @@ namespace Avalonia.Android private readonly AutoResetEvent _event = new(false); private readonly GCHandle _timerHandle; private readonly HashSet _views = new(); - private Action? _tick; + private bool _pendingCallback; private long _lastTime; - private int _count; public ChoreographerTimer() { @@ -40,28 +39,13 @@ namespace Avalonia.Android public bool RunsInBackground => true; - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - lock (_lock) - { - _tick += value; - _count++; - - if (_count == 1) - { - PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); - } - } - } - remove - { - lock (_lock) - { - _tick -= value; - _count--; - } + _tick = value; + PostFrameCallbackIfNeeded(); } } @@ -70,20 +54,14 @@ namespace Avalonia.Android lock (_lock) { _views.Add(view); - - if (_views.Count == 1) - { - PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); - } + PostFrameCallbackIfNeeded(); } return Disposable.Create( () => { - lock (_lock) - { + lock (_lock) _views.Remove(view); - } } ); } @@ -109,14 +87,28 @@ namespace Avalonia.Android } } + private void PostFrameCallbackIfNeeded() + { + lock (_lock) + { + if(_pendingCallback) + return; + + if (_tick == null || _views.Count == 0) + return; + + _pendingCallback = true; + + PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); + } + } + private void DoFrameCallback(long frameTimeNanos, IntPtr data) { lock (_lock) { - if (_count > 0 && _views.Count > 0) - { - PostFrameCallback(_choreographer.Task.Result, data); - } + _pendingCallback = false; + PostFrameCallbackIfNeeded(); _lastTime = frameTimeNanos; _event.Set(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 2398468456..2acda6c57d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -51,7 +51,7 @@ namespace Avalonia.Rendering.Composition /// [PrivateApi] public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) - : this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits) + : this(AvaloniaLocator.Current.GetRequiredService(), gpu, useUiThreadForSynchronousCommits) { } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index f8382547b9..81a3c09b35 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -39,6 +39,11 @@ namespace Avalonia.Rendering.Composition.Server public ICompositionTargetDebugEvents? DebugEvents { get; set; } public int RenderedVisuals { get; set; } public int VisitedVisuals { get; set; } + + /// + /// Returns true if the target is enabled and has pending work but its render target was not ready. + /// + internal bool IsWaitingForReadyRenderTarget { get; private set; } public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) @@ -125,6 +130,8 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + IsWaitingForReadyRenderTarget = false; + if (_disposed) return; @@ -143,11 +150,15 @@ namespace Avalonia.Rendering.Composition.Server try { if (_renderTarget == null && !_compositor.IsReadyToCreateRenderTarget(_surfaces())) + { + IsWaitingForReadyRenderTarget = IsEnabled; return; + } _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); } catch (RenderTargetNotReadyException) { + IsWaitingForReadyRenderTarget = IsEnabled; return; } catch (RenderTargetCorruptedException) @@ -164,7 +175,10 @@ namespace Avalonia.Rendering.Composition.Server return; if (!_renderTarget.IsReady) + { + IsWaitingForReadyRenderTarget = IsEnabled; return; + } var needLayer = _overlays.RequireLayer // Check if we don't need overlays // Check if render target can be rendered to directly and preserves the previous frame diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 76e649407f..b8cc5afca2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -44,6 +44,9 @@ namespace Avalonia.Rendering.Composition.Server public CompositionOptions Options { get; } public ServerCompositorAnimations Animations { get; } public ReadbackIndices Readback { get; } = new(); + + private int _ticksSinceLastCommit; + private const int CommitGraceTicks = 10; public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, CompositionOptions options, @@ -64,6 +67,7 @@ namespace Avalonia.Rendering.Composition.Server { lock (_batches) _batches.Enqueue(batch); + _renderLoop.Wakeup(); } internal void UpdateServerTime() => ServerNow = Clock.Elapsed; @@ -72,6 +76,7 @@ namespace Avalonia.Rendering.Composition.Server readonly List _reusableToNotifyRenderedList = new(); void ApplyPendingBatches() { + bool hadBatches = false; while (true) { CompositionBatch batch; @@ -119,7 +124,13 @@ namespace Avalonia.Rendering.Composition.Server _reusableToNotifyProcessedList.Add(batch); LastBatchId = batch.SequenceId; + hadBatches = true; } + + if (hadBatches) + _ticksSinceLastCommit = 0; + else if (_ticksSinceLastCommit < int.MaxValue) + _ticksSinceLastCommit++; } void ReadServerJobs(BatchStreamReader reader, Queue queue, object endMarker) @@ -171,8 +182,10 @@ namespace Avalonia.Rendering.Composition.Server _reusableToNotifyRenderedList.Clear(); } - public void Render() => Render(true); - public void Render(bool catchExceptions) + bool IRenderLoopTask.Render() => ExecuteRender(true); + public void Render(bool catchExceptions) => ExecuteRender(catchExceptions); + + private bool ExecuteRender(bool catchExceptions) { if (Dispatcher.UIThread.CheckAccess()) { @@ -182,7 +195,7 @@ namespace Avalonia.Rendering.Composition.Server try { using (Dispatcher.UIThread.DisableProcessing()) - RenderReentrancySafe(catchExceptions); + return RenderReentrancySafe(catchExceptions); } finally { @@ -190,10 +203,10 @@ namespace Avalonia.Rendering.Composition.Server } } else - RenderReentrancySafe(catchExceptions); + return RenderReentrancySafe(catchExceptions); } - private void RenderReentrancySafe(bool catchExceptions) + private bool RenderReentrancySafe(bool catchExceptions) { lock (_lock) { @@ -202,7 +215,7 @@ namespace Avalonia.Rendering.Composition.Server try { _safeThread = Thread.CurrentThread; - RenderCore(catchExceptions); + return RenderCore(catchExceptions); } finally { @@ -235,17 +248,16 @@ namespace Avalonia.Rendering.Composition.Server return Stopwatch.GetElapsedTime(compositorGlobalPassesStarted); } - private void RenderCore(bool catchExceptions) + private bool RenderCore(bool catchExceptions) { - UpdateServerTime(); var compositorGlobalPassesElapsed = ExecuteGlobalPasses(); try { - if(!RenderInterface.IsReady) - return; + if (!RenderInterface.IsReady) + return true; RenderInterface.EnsureValidBackendContext(); ExecuteServerJobs(_receivedJobQueue); @@ -263,6 +275,18 @@ namespace Avalonia.Rendering.Composition.Server { Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e); } + + // Request a tick if we have active animations or if there are recent batches + if (Animations.NeedNextTick || _ticksSinceLastCommit < CommitGraceTicks) + return true; + + // Request a tick if we had unready targets in the last tick, to check if they are ready next time + foreach (var target in _activeTargets) + if (target.IsWaitingForReadyRenderTarget) + return true; + + // Otherwise there is no need to waste CPU cycles, tell the timer to pause + return false; } public void AddCompositionTarget(ServerCompositionTarget target) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs index 1f2c7dedb8..0e59cd8f03 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs @@ -30,6 +30,8 @@ internal class ServerCompositorAnimations _dirtyAnimatedObjects.Clear(); } + public bool NeedNextTick => _clockItems.Count > 0; + public void AddDirtyAnimatedObject(ServerObjectAnimations obj) { if (_dirtyAnimatedObjects.Add(obj)) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 7e1c9e711f..d9003659a1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -13,52 +14,75 @@ namespace Avalonia.Rendering.Composition.Transport; /// internal abstract class BatchStreamPoolBase : IDisposable { + private readonly Action>? _startTimer; readonly Stack _pool = new(); bool _disposed; int _usage; readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; - readonly bool _reclaimImmediately; + private readonly WeakReference> _updateRef; + private readonly Dispatcher? _reclaimOnDispatcher; + private bool _timerIsRunning; + private ulong _currentUpdateTick, _lastActivityTick; public int CurrentUsage => _usage; public int CurrentPool => _pool.Count; public BatchStreamPoolBase(bool needsFinalize, bool reclaimImmediately, Action>? startTimer = null) { + _startTimer = startTimer; if(!needsFinalize) - GC.SuppressFinalize(needsFinalize); + GC.SuppressFinalize(this); - var updateRef = new WeakReference>(this); - if ( - reclaimImmediately - || Dispatcher.FromThread(Thread.CurrentThread) == null) - _reclaimImmediately = true; - else - StartUpdateTimer(startTimer, updateRef); + _updateRef = new WeakReference>(this); + _reclaimOnDispatcher = !reclaimImmediately ? Dispatcher.FromThread(Thread.CurrentThread) : null; + EnsureUpdateTimer(); } + - static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) + void EnsureUpdateTimer() { - Func timerProc = () => + if (_timerIsRunning || !NeedsTimer) + return; + + var timerProc = GetTimerProc(_updateRef); + + if (_startTimer != null) + _startTimer(timerProc); + else { - if (updateRef.TryGetTarget(out var target)) + if (_reclaimOnDispatcher != null) { - target.UpdateStatistics(); - return true; + if (_reclaimOnDispatcher.CheckAccess()) + DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); + else + _reclaimOnDispatcher.Post( + () => DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)), + DispatcherPriority.Normal); } + } + + _timerIsRunning = true; + // Explicit capture + static Func GetTimerProc(WeakReference> updateRef) => () => + { + if (updateRef.TryGetTarget(out var target)) + return target.UpdateTimerTick(); return false; }; - if (startTimer != null) - startTimer(timerProc); - else - DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); } - private void UpdateStatistics() + [MemberNotNullWhen(true, nameof(_reclaimOnDispatcher))] + private bool NeedsTimer => _reclaimOnDispatcher != null && + _currentUpdateTick - _lastActivityTick < (uint)_usageStatistics.Length * 2 + 1; + private bool ReclaimImmediately => _reclaimOnDispatcher == null; + + private bool UpdateTimerTick() { lock (_pool) { + _currentUpdateTick++; var maximumUsage = _usageStatistics.Max(); var recentlyUsedPooledSlots = maximumUsage - _usage; var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); @@ -67,9 +91,17 @@ internal abstract class BatchStreamPoolBase : IDisposable _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; _usageStatistics[_usageStatisticsSlot] = 0; + + return _timerIsRunning = NeedsTimer; } } + private void OnActivity() + { + _lastActivityTick = _currentUpdateTick; + EnsureUpdateTimer(); + } + protected abstract T CreateItem(); protected virtual void ClearItem(T item) @@ -90,6 +122,8 @@ internal abstract class BatchStreamPoolBase : IDisposable if (_usageStatistics[_usageStatisticsSlot] < _usage) _usageStatistics[_usageStatisticsSlot] = _usage; + OnActivity(); + if (_pool.Count != 0) return _pool.Pop(); } @@ -103,9 +137,10 @@ internal abstract class BatchStreamPoolBase : IDisposable lock (_pool) { _usage--; - if (!_disposed && !_reclaimImmediately) + if (!_disposed && !ReclaimImmediately) { _pool.Push(item); + OnActivity(); return; } } diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index 102cc30e87..cc24086305 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -15,8 +15,7 @@ namespace Avalonia.Rendering [PrivateApi] public class DefaultRenderTimer : IRenderTimer { - private int _subscriberCount; - private Action? _tick; + private volatile Action? _tick; private IDisposable? _subscription; /// @@ -36,40 +35,28 @@ namespace Avalonia.Rendering public int FramesPerSecond { get; } /// - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - _tick += value; - - if (_subscriberCount++ == 0) + if (value != null) { - Start(); + _tick = value; + _subscription ??= StartCore(InternalTick); } - } - - remove - { - if (--_subscriberCount == 0) + else { - Stop(); + _subscription?.Dispose(); + _subscription = null; + _tick = null; } - - _tick -= value; } } /// public virtual bool RunsInBackground => true; - /// - /// Starts the timer. - /// - protected void Start() - { - _subscription = StartCore(InternalTick); - } - /// /// Provides the implementation of starting the timer. /// @@ -85,15 +72,6 @@ namespace Avalonia.Rendering return new Timer(_ => tick(TimeSpan.FromMilliseconds(Environment.TickCount)), null, interval, interval); } - /// - /// Stops the timer. - /// - protected void Stop() - { - _subscription?.Dispose(); - _subscription = null; - } - private void InternalTick(TimeSpan tickCount) { _tick?.Invoke(tickCount); diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index bf2c221b03..e887832ebc 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -9,8 +9,8 @@ namespace Avalonia.Rendering /// The render loop is responsible for advancing the animation timer and updating the scene /// graph for visible windows. /// - [NotClientImplementable] - internal interface IRenderLoop + [PrivateApi] + public interface IRenderLoop { /// /// Adds an update task. @@ -20,17 +20,23 @@ namespace Avalonia.Rendering /// Registered update tasks will be polled on each tick of the render loop after the /// animation timer has been pulsed. /// - void Add(IRenderLoopTask i); + internal void Add(IRenderLoopTask i); /// /// Removes an update task. /// /// The update task. - void Remove(IRenderLoopTask i); + internal void Remove(IRenderLoopTask i); /// /// Indicates if the rendering is done on a non-UI thread. /// - bool RunsInBackground { get; } + internal bool RunsInBackground { get; } + + /// + /// Wakes up the render loop to schedule the next tick. + /// Thread-safe: can be called from any thread. + /// + internal void Wakeup(); } } diff --git a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs b/src/Avalonia.Base/Rendering/IRenderLoopTask.cs index f63855e651..67416cc155 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoopTask.cs @@ -1,10 +1,7 @@ -using System; -using System.Threading.Tasks; - namespace Avalonia.Rendering { internal interface IRenderLoopTask { - void Render(); + bool Render(); } } diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index 396e84d492..772dcf7656 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -10,16 +10,19 @@ namespace Avalonia.Rendering public interface IRenderTimer { /// - /// Raised when the render timer ticks to signal a new frame should be drawn. + /// Gets or sets the callback to be invoked when the timer ticks. + /// This property can be set from any thread, but it's guaranteed that it's not set concurrently + /// (i. e. render loop always does it under a lock). + /// Setting the value to null suggests the timer to stop ticking, however + /// timer is allowed to produce ticks on the previously set value as long as it stops doing so /// /// - /// This event can be raised on any thread; it is the responsibility of the subscriber to - /// switch execution to the right thread. + /// The callback can be invoked on any thread /// - event Action Tick; + Action? Tick { get; set; } /// - /// Indicates if the timer ticks on a non-UI thread + /// Indicates if the timer ticks on a non-UI thread. /// bool RunsInBackground { get; } } diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 846cce7a23..9af9c54443 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -2,58 +2,52 @@ using System.Collections.Generic; using System.Threading; using Avalonia.Logging; +using Avalonia.Metadata; using Avalonia.Threading; namespace Avalonia.Rendering { /// - /// The application render loop. + /// Provides factory methods for creating instances. + /// + [PrivateApi] + public static class RenderLoop + { + /// + /// Creates an from an . + /// + public static IRenderLoop FromTimer(IRenderTimer timer) => new DefaultRenderLoop(timer); + } + + /// + /// Default implementation of the application render loop. /// /// /// The render loop is responsible for advancing the animation timer and updating the scene - /// graph for visible windows. + /// graph for visible windows. It owns the sleep/wake state machine: setting + /// to a non-null callback to start the timer and to null to + /// stop it, under a lock so that timer implementations never see concurrent changes. /// - internal class RenderLoop : IRenderLoop + internal class DefaultRenderLoop : IRenderLoop { private readonly List _items = new List(); private readonly List _itemsCopy = new List(); - private IRenderTimer? _timer; + private Action _tick; + private readonly IRenderTimer _timer; + private readonly object _timerLock = new(); private int _inTick; - - public static IRenderLoop LocatorAutoInstance - { - get - { - var loop = AvaloniaLocator.Current.GetService(); - if (loop == null) - { - var timer = AvaloniaLocator.Current.GetRequiredService(); - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(loop = new RenderLoop(timer)); - } - - return loop; - } - } + private volatile bool _hasItems; + private bool _running; + private bool _wakeupPending; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The render timer. - public RenderLoop(IRenderTimer timer) + public DefaultRenderLoop(IRenderTimer timer) { _timer = timer; - } - - /// - /// Gets the render timer. - /// - protected IRenderTimer Timer - { - get - { - return _timer ??= AvaloniaLocator.Current.GetRequiredService(); - } + _tick = TimerTick; } /// @@ -62,14 +56,17 @@ namespace Avalonia.Rendering _ = i ?? throw new ArgumentNullException(nameof(i)); Dispatcher.UIThread.VerifyAccess(); + bool shouldStart; lock (_items) { _items.Add(i); + shouldStart = _items.Count == 1; + } - if (_items.Count == 1) - { - Timer.Tick += TimerTick; - } + if (shouldStart) + { + _hasItems = true; + Wakeup(); } } @@ -78,19 +75,48 @@ namespace Avalonia.Rendering { _ = i ?? throw new ArgumentNullException(nameof(i)); Dispatcher.UIThread.VerifyAccess(); + + bool shouldStop; lock (_items) { _items.Remove(i); + shouldStop = _items.Count == 0; + } - if (_items.Count == 0) + if (shouldStop) + { + _hasItems = false; + lock (_timerLock) { - Timer.Tick -= TimerTick; + if (_running) + { + _running = false; + _wakeupPending = false; + _timer.Tick = null; + } } } } /// - public bool RunsInBackground => Timer.RunsInBackground; + public bool RunsInBackground => _timer.RunsInBackground; + + /// + public void Wakeup() + { + lock (_timerLock) + { + if (_hasItems && !_running) + { + _running = true; + _timer.Tick = _tick; + } + else + { + _wakeupPending = true; + } + } + } private void TimerTick(TimeSpan time) { @@ -98,21 +124,49 @@ namespace Avalonia.Rendering { try { - + // Consume any pending wakeup — this tick will process its work. + // Only wakeups arriving during task execution will keep the timer running. + // Also drop late ticks that arrive after the timer was stopped. + lock (_timerLock) + { + if (!_running) + return; + _wakeupPending = false; + } + lock (_items) { _itemsCopy.Clear(); _itemsCopy.AddRange(_items); } - + var wantsNextTick = false; for (int i = 0; i < _itemsCopy.Count; i++) { - _itemsCopy[i].Render(); + wantsNextTick |= _itemsCopy[i].Render(); } _itemsCopy.Clear(); + if (!wantsNextTick) + { + lock (_timerLock) + { + if (!_running) + { + // Already stopped by Remove() + } + else if (_wakeupPending) + { + _wakeupPending = false; + } + else + { + _running = false; + _timer.Tick = null; + } + } + } } catch (Exception ex) { diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 3ad4ea94d0..570dc4cb30 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -8,10 +8,10 @@ namespace Avalonia.Rendering [PrivateApi] public class SleepLoopRenderTimer : IRenderTimer { - private Action? _tick; - private int _count; - private readonly object _lock = new object(); - private bool _running; + private volatile Action? _tick; + private volatile bool _stopped = true; + private bool _threadStarted; + private readonly AutoResetEvent _wakeEvent = new(false); private readonly Stopwatch _st = Stopwatch.StartNew(); private readonly TimeSpan _timeBetweenTicks; @@ -19,28 +19,30 @@ namespace Avalonia.Rendering { _timeBetweenTicks = TimeSpan.FromSeconds(1d / fps); } - - public event Action Tick + + public Action? Tick { - add + get => _tick; + set { - lock (_lock) + if (value != null) { - _tick += value; - _count++; - if (_running) - return; - _running = true; - new Thread(LoopProc) { IsBackground = true }.Start(); + _tick = value; + _stopped = false; + if (!_threadStarted) + { + _threadStarted = true; + new Thread(LoopProc) { IsBackground = true }.Start(); + } + else + { + _wakeEvent.Set(); + } } - - } - remove - { - lock (_lock) + else { - _tick -= value; - _count--; + _stopped = true; + _tick = null; } } } @@ -52,24 +54,17 @@ namespace Avalonia.Rendering var lastTick = _st.Elapsed; while (true) { + if (_stopped) + _wakeEvent.WaitOne(); + var now = _st.Elapsed; var timeTillNextTick = lastTick + _timeBetweenTicks - now; - if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); + if (timeTillNextTick.TotalMilliseconds > 1) + _wakeEvent.WaitOne(timeTillNextTick); lastTick = now = _st.Elapsed; - lock (_lock) - { - if (_count == 0) - { - _running = false; - return; - } - } _tick?.Invoke(now); - } } - - } } diff --git a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs index 0f3387cd1a..d15d3a052e 100644 --- a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs @@ -12,8 +12,9 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer private readonly Stopwatch _stopwatch; private readonly Thread _timerThread; private readonly AutoResetEvent _autoResetEvent; - private Action? _tick; - private int _subscriberCount; + private readonly object _lock = new(); + private volatile Action? _tick; + private volatile bool _active; private bool _registered; public ThreadProxyRenderTimer(IRenderTimer inner, int maxStackSize = 1 * 1024 * 1024) @@ -24,33 +25,54 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer _timerThread = new Thread(RenderTimerThreadFunc, maxStackSize) { Name = "RenderTimerLoop", IsBackground = true }; } - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - _tick += value; - - if (!_registered) + lock (_lock) { - _registered = true; - _timerThread.Start(); + if (value != null) + { + _tick = value; + _active = true; + EnsureStarted(); + _inner.Tick = InnerTick; + } + else + { + // Don't set _inner.Tick = null here — may be on the wrong thread. + // InnerTick will detect _active=false and clear _inner.Tick on the correct thread. + _active = false; + _tick = null; + } } + } + } - if (_subscriberCount++ == 0) - { - _inner.Tick += InnerTick; - } + public bool RunsInBackground => true; + + private void EnsureStarted() + { + if (!_registered) + { + _registered = true; + _stopwatch.Start(); + _timerThread.Start(); } + } - remove + private void InnerTick(TimeSpan obj) + { + lock (_lock) { - if (--_subscriberCount == 0) + if (!_active) { - _inner.Tick -= InnerTick; + _inner.Tick = null; + return; } - - _tick -= value; } + _autoResetEvent.Set(); } private void RenderTimerThreadFunc() @@ -60,11 +82,4 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer _tick?.Invoke(_stopwatch.Elapsed); } } - - private void InnerTick(TimeSpan obj) - { - _autoResetEvent.Set(); - } - - public bool RunsInBackground => true; } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 43eddb010d..bc94ff4388 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -55,7 +55,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToSingleton() - .Bind().ToConstant(new UiThreadRenderTimer(60)) + .Bind().ToConstant(RenderLoop.FromTimer(new UiThreadRenderTimer(60))) .Bind().ToConstant(instance) .Bind().ToSingleton() .Bind().ToSingleton(); diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index d13d442e09..d9c8e333cb 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -69,16 +69,11 @@ namespace Avalonia.DesignerSupport.Remote private sealed class DummyRenderTimer : IRenderTimer { - public event Action Tick - { - add { } - remove { } - } - + public Action? Tick { get; set; } public bool RunsInBackground => false; } - public Compositor Compositor { get; } = new(new RenderLoop(new DummyRenderTimer()), null); + public Compositor Compositor { get; } = new(RenderLoop.FromTimer(new DummyRenderTimer()), null); public void Dispose() { diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 34f0ee766e..825eb254be 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -122,7 +122,7 @@ namespace Avalonia.Native .Bind().ToConstant(this) .Bind().ToConstant(clipboardImpl) .Bind().ToConstant(clipboard) - .Bind().ToConstant(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))) + .Bind().ToConstant(RenderLoop.FromTimer(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())))) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) .Bind().ToConstant(applicationPlatform) diff --git a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs index 625de5d6bc..d484b82ac7 100644 --- a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs +++ b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs @@ -9,9 +9,8 @@ internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTim { private readonly IAvnPlatformRenderTimer _platformRenderTimer; private readonly Stopwatch _stopwatch; - private Action? _tick; - private int _subscriberCount; - private bool registered; + private volatile Action? _tick; + private bool _registered; public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer) { @@ -19,42 +18,41 @@ internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTim _stopwatch = Stopwatch.StartNew(); } - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - _tick += value; - - if (!registered) + if (value != null) { - registered = true; - var registrationResult = _platformRenderTimer.RegisterTick(this); - if (registrationResult != 0) - { - throw new InvalidOperationException( - $"Avalonia.Native was not able to start the RenderTimer. Native error code is: {registrationResult}"); - } + _tick = value; + EnsureRegistered(); + _platformRenderTimer.Start(); } - - if (_subscriberCount++ == 0) + else { - _platformRenderTimer.Start(); + _platformRenderTimer.Stop(); + _tick = null; } } + } - remove + public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool(); + + private void EnsureRegistered() + { + if (!_registered) { - if (--_subscriberCount == 0) + _registered = true; + var registrationResult = _platformRenderTimer.RegisterTick(this); + if (registrationResult != 0) { - _platformRenderTimer.Stop(); + throw new InvalidOperationException( + $"Avalonia.Native was not able to start the RenderTimer. Native error code is: {registrationResult}"); } - - _tick -= value; } } - public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool(); - public void Run() { _tick?.Invoke(_stopwatch.Elapsed); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index bff986d2d1..566b0d907a 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -87,7 +87,7 @@ namespace Avalonia.X11 : new X11PlatformThreading(this); Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(timer) + .Bind().ToConstant(RenderLoop.FromTimer(timer)) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")) .Bind().ToFunc(() => KeyboardDevice) diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs index fea94d0248..de9a167954 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs @@ -18,19 +18,16 @@ internal class BrowserRenderTimer : IRenderTimer public bool RunsInBackground { get; } - public event Action? Tick + public Action? Tick { - add + set { if (!BrowserWindowingPlatform.IsThreadingEnabled) StartOnThisThread(); - _tick += value; - } - remove - { - _tick -= value; + _tick = value; } + get => _tick; } public void StartOnThisThread() @@ -50,4 +47,4 @@ internal class BrowserRenderTimer : IRenderTimer tick.Invoke(TimeSpan.FromMilliseconds(timestamp)); } } -} +} \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs index 8d454ff582..1d9d1248b8 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs @@ -9,5 +9,5 @@ internal static class BrowserSharedRenderLoop { private static BrowserRenderTimer? s_browserUiRenderTimer; public static BrowserRenderTimer RenderTimer => s_browserUiRenderTimer ??= new BrowserRenderTimer(false); - public static Lazy RenderLoop = new(() => new RenderLoop(RenderTimer), true); + public static Lazy RenderLoop = new(() => Avalonia.Rendering.RenderLoop.FromTimer(RenderTimer), true); } diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b56e686d4b..8e44942d32 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -15,6 +15,7 @@ namespace Avalonia.Headless public static class AvaloniaHeadlessPlatform { internal static Compositor? Compositor { get; private set; } + private static RenderTimer? s_renderTimer; private class RenderTimer : DefaultRenderTimer { @@ -85,7 +86,7 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(new KeyboardDevice()) - .Bind().ToConstant(new RenderTimer(60)) + .Bind().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer = new RenderTimer(60))) .Bind().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat)) .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })); @@ -99,9 +100,8 @@ namespace Avalonia.Headless /// Count of frames to be ticked on the timer. public static void ForceRenderTimerTick(int count = 1) { - var timer = AvaloniaLocator.Current.GetService() as RenderTimer; for (var c = 0; c < count; c++) - timer?.ForceTick(); + s_renderTimer?.ForceTick(); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index ee8b85919e..3239957d73 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -64,7 +64,7 @@ namespace Avalonia.LinuxFramebuffer Dispatcher.InitializeUIThreadDispatcher(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(timer) + .Bind().ToConstant(RenderLoop.FromTimer(timer)) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() diff --git a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs index 6c73fbeb9e..55d49ca30c 100644 --- a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs +++ b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs @@ -21,15 +21,36 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor { private static readonly Guid IID_IDCompositionDesktopDevice = Guid.Parse("5f4633fe-1e08-4cb8-8c75-ce24333f5602"); - public event Action? Tick; + private volatile Action? _tick; public bool RunsInBackground => true; private readonly DirectCompositionShared _shared; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; public DirectCompositionConnection(DirectCompositionShared shared) { _shared = shared; } + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } private static bool TryCreateAndRegisterCore() { @@ -52,7 +73,7 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor } AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connect)); tcs.SetResult(true); } catch (Exception e) @@ -81,8 +102,11 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor { try { + if (_stopped) + WaitHandle.WaitAny([_wakeEvent, cts.Token.WaitHandle]); + device.WaitForCommitCompletion(); - Tick?.Invoke(_stopwatch.Elapsed); + _tick?.Invoke(_stopwatch.Elapsed); } catch (Exception ex) { diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs index 678b15e0d7..9ee2f25c86 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Platform.Surfaces; @@ -25,8 +26,10 @@ namespace Avalonia.Win32.DirectX public bool RunsInBackground => true; - public event Action? Tick; + private volatile Action? _tick; private readonly object _syncLock; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; private IDXGIOutput? _output; @@ -37,6 +40,25 @@ namespace Avalonia.Win32.DirectX { _syncLock = syncLock; } + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } public static bool TryCreateAndRegister() { @@ -70,6 +92,9 @@ namespace Avalonia.Win32.DirectX { try { + if (_stopped) + _wakeEvent.WaitOne(); + lock (_syncLock) { if (_output is not null) @@ -94,7 +119,7 @@ namespace Avalonia.Win32.DirectX // but theoretically someone could have a weirder setup out there DwmFlush(); } - Tick?.Invoke(_stopwatch.Elapsed); + _tick?.Invoke(_stopwatch.Elapsed); } } catch (Exception ex) @@ -199,7 +224,7 @@ namespace Avalonia.Win32.DirectX var connection = new DxgiConnection(pumpLock); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connection)); tcs.SetResult(true); connection.RunLoop(); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f158d539ff..7903a62d8f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -98,7 +98,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(renderTimer) + .Bind().ToConstant(RenderLoop.FromTimer(renderTimer)) .Bind().ToConstant(s_instance) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control) { diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs index fa7ff2e7a3..d4cdfcbe42 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -17,8 +17,29 @@ namespace Avalonia.Win32.WinRT.Composition; internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFactory { private readonly WinUiCompositionShared _shared; - public event Action? Tick; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; + private volatile Action? _tick; public bool RunsInBackground => true; + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } public WinUiCompositorConnection() { @@ -58,7 +79,7 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa }); connect = new WinUiCompositorConnection(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connect)); tcs.SetResult(true); } @@ -102,8 +123,11 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa { _currentCommit?.Dispose(); _currentCommit = null; - _parent.Tick?.Invoke(_st.Elapsed); + _parent._tick?.Invoke(_st.Elapsed); + // Always schedule a commit so the current frame's work reaches DWM. ScheduleNextCommit(); + if (_parent._stopped) + _parent._wakeEvent.WaitOne(); } private void ScheduleNextCommit() diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs index 676554811e..e8a313afa8 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.Threading; -using System.Threading.Tasks; using Avalonia.Rendering; using CoreAnimation; using Foundation; @@ -11,7 +10,7 @@ namespace Avalonia.iOS { class DisplayLinkTimer : IRenderTimer { - public event Action? Tick; + private volatile Action? _tick; private Stopwatch _st = Stopwatch.StartNew(); public DisplayLinkTimer() @@ -31,9 +30,16 @@ namespace Avalonia.iOS public bool RunsInBackground => true; + // TODO: start/stop on RenderLoop request + public Action? Tick + { + get => _tick; + set => _tick = value; + } + private void OnLinkTick() { - Tick?.Invoke(_st.Elapsed); + _tick?.Invoke(_st.Elapsed); } } } diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 29633a8609..79926a3836 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -93,7 +93,7 @@ namespace Avalonia.iOS { Key.PageUp , "⇞" }, { Key.Right , "→" }, { Key.Space , "␣" }, { Key.Tab , "⇥" }, { Key.Up , "↑" } }, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥")) - .Bind().ToConstant(Timer) + .Bind().ToConstant(RenderLoop.FromTimer(Timer)) .Bind().ToConstant(keyboard); if (appDelegate is not null) diff --git a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs index cf6d7a8aee..21ac9c1ae1 100644 --- a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs @@ -88,7 +88,7 @@ public class CompositionAnimationTests : ScopedTestBase { using var scope = AvaloniaLocator.EnterScope(); var compositor = - new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null); + new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null); var target = compositor.CreateSolidColorVisual(); var ani = new ScalarKeyFrameAnimation(compositor); foreach (var frame in data.Frames) diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs index 7451e3c843..06df626857 100644 --- a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs +++ b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs @@ -22,7 +22,7 @@ public class CompositionTargetUpdateOnly : IDisposable class Timer : IRenderTimer { - event Action IRenderTimer.Tick { add { } remove { } } + public Action Tick { get; set; } = null!; public bool RunsInBackground => false; } @@ -52,7 +52,7 @@ public class CompositionTargetUpdateOnly : IDisposable { _includeRender = includeRender; _app = UnitTestApplication.Start(TestServices.StyledWindow); - _compositor = new Compositor(new RenderLoop(new Timer()), null, true, new ManualScheduler(), true, + _compositor = new Compositor(RenderLoop.FromTimer(new Timer()), null, true, new ManualScheduler(), true, Dispatcher.UIThread, null); _target = _compositor.CreateCompositionTarget(() => [new NullFramebuffer()]); _target.PixelSize = new PixelSize(1000, 1000); @@ -99,7 +99,7 @@ public class CompositionTargetUpdateOnly : IDisposable { _target.Root.Offset = new Vector3D(_target.Root.Offset.X == 0 ? 1 : 0, 0, 0); _compositor.Commit(); - _compositor.Server.Render(); + _compositor.Server.Render(false); if (!_includeRender) _target.Server.Update(); diff --git a/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs b/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs index 588785cf69..5c44809a85 100644 --- a/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs +++ b/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs @@ -47,7 +47,7 @@ public class DirectFbCompositionTests : TestBase void Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised(bool advertised) { var timer = new ManualRenderTimer(); - var compositor = new Compositor(new RenderLoop(timer), null, true, + var compositor = new Compositor(RenderLoop.FromTimer(timer), null, true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread, new CompositionOptions { UseRegionDirtyRectClipping = true diff --git a/tests/Avalonia.RenderTests/ManualRenderTimer.cs b/tests/Avalonia.RenderTests/ManualRenderTimer.cs index 6247691453..1b6e7539a0 100644 --- a/tests/Avalonia.RenderTests/ManualRenderTimer.cs +++ b/tests/Avalonia.RenderTests/ManualRenderTimer.cs @@ -5,7 +5,7 @@ namespace Avalonia.Skia.RenderTests { public class ManualRenderTimer : IRenderTimer { - public event Action? Tick; + public Action? Tick { get; set; } public bool RunsInBackground => false; public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); } diff --git a/tests/Avalonia.RenderTests/TestRenderHelper.cs b/tests/Avalonia.RenderTests/TestRenderHelper.cs index 68cf05e9b9..87132881f7 100644 --- a/tests/Avalonia.RenderTests/TestRenderHelper.cs +++ b/tests/Avalonia.RenderTests/TestRenderHelper.cs @@ -63,7 +63,7 @@ static class TestRenderHelper { var timer = new ManualRenderTimer(); - var compositor = new Compositor(new RenderLoop(timer), null, true, + var compositor = new Compositor(RenderLoop.FromTimer(timer), null, true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread); using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat)) diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index e90e1cff0e..cb2a84049c 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -42,9 +42,10 @@ public class CompositorTestServices : IDisposable _app = UnitTestApplication.Start(services); try { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(Timer); + var renderLoop = RenderLoop.FromTimer(Timer); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(renderLoop); - Compositor = new Compositor(new RenderLoop(Timer), null, + Compositor = new Compositor(renderLoop, null, true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread); var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000)); TopLevel = new EmbeddableControlRoot(impl) @@ -136,7 +137,7 @@ public class CompositorTestServices : IDisposable public class ManualRenderTimer : IRenderTimer { - public event Action? Tick; + public Action? Tick { get; set; } public bool RunsInBackground => false; public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); } diff --git a/tests/Avalonia.UnitTests/RendererMocks.cs b/tests/Avalonia.UnitTests/RendererMocks.cs index 32d171e147..9b172fe342 100644 --- a/tests/Avalonia.UnitTests/RendererMocks.cs +++ b/tests/Avalonia.UnitTests/RendererMocks.cs @@ -17,7 +17,7 @@ namespace Avalonia.UnitTests } public static Compositor CreateDummyCompositor() => - new(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null, false, + new(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null, false, new CompositionCommitScheduler(), true, Dispatcher.UIThread); class CompositionCommitScheduler : ICompositorScheduler From 45bf966051797f6fbb8287b0afe363b9a9a884cc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 14 Mar 2026 00:22:18 +0900 Subject: [PATCH 19/24] Enable recursive submodule checkout in workflow --- .github/workflows/update-api.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-api.yml b/.github/workflows/update-api.yml index 611a4ead50..27a0598d3b 100644 --- a/.github/workflows/update-api.yml +++ b/.github/workflows/update-api.yml @@ -71,6 +71,7 @@ jobs: with: ref: ${{ steps.pr.outputs.sha }} token: ${{ secrets.GITHUB_TOKEN }} + submodules: recursive - name: Setup .NET uses: actions/setup-dotnet@v4 From e327dcc54de0e12e74f02458f8ab8615270ee6f2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 14 Mar 2026 00:22:54 +0900 Subject: [PATCH 20/24] Add recursive submodule checkout to API diff workflow Enable recursive submodule checkout in API diff workflow. --- .github/workflows/api-diff.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/api-diff.yml b/.github/workflows/api-diff.yml index f855380f9e..e87f3f334f 100644 --- a/.github/workflows/api-diff.yml +++ b/.github/workflows/api-diff.yml @@ -70,6 +70,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ steps.pr.outputs.sha }} + submodules: recursive - name: Setup .NET uses: actions/setup-dotnet@v4 From ada1e72ce8dce25bfdf58fe5f848d7f6652e7b00 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 13 Mar 2026 18:12:19 +0100 Subject: [PATCH 21/24] Hide TextRange. It has no public usage. (#20893) * Hide TextRange. It has no public usage. * Update api diff --- api/Avalonia.Headless.XUnit.nupkg.xml | 4 +- api/Avalonia.Headless.nupkg.xml | 4 +- api/Avalonia.LinuxFramebuffer.nupkg.xml | 4 +- api/Avalonia.Skia.nupkg.xml | 4 +- api/Avalonia.Win32.Interoperability.nupkg.xml | 4 +- api/Avalonia.nupkg.xml | 16 ++++- .../TextFormatting/FormattedTextSource.cs | 66 +++++++++++++++++ .../Media/TextFormatting/TextRange.cs | 70 ------------------- .../TextFormatting/MultiBufferTextSource.cs | 5 +- 9 files changed, 92 insertions(+), 85 deletions(-) delete mode 100644 src/Avalonia.Base/Media/TextFormatting/TextRange.cs diff --git a/api/Avalonia.Headless.XUnit.nupkg.xml b/api/Avalonia.Headless.XUnit.nupkg.xml index 15a56561b9..c87cf909fe 100644 --- a/api/Avalonia.Headless.XUnit.nupkg.xml +++ b/api/Avalonia.Headless.XUnit.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -73,4 +73,4 @@ baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll - \ No newline at end of file + diff --git a/api/Avalonia.Headless.nupkg.xml b/api/Avalonia.Headless.nupkg.xml index 435df92d13..229047057a 100644 --- a/api/Avalonia.Headless.nupkg.xml +++ b/api/Avalonia.Headless.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll - \ No newline at end of file + diff --git a/api/Avalonia.LinuxFramebuffer.nupkg.xml b/api/Avalonia.LinuxFramebuffer.nupkg.xml index 10c927a203..0fa6ef4e03 100644 --- a/api/Avalonia.LinuxFramebuffer.nupkg.xml +++ b/api/Avalonia.LinuxFramebuffer.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll current/Avalonia.LinuxFramebuffer/lib/net8.0/Avalonia.LinuxFramebuffer.dll - \ No newline at end of file + diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml index c1afe2f966..b73745af8e 100644 --- a/api/Avalonia.Skia.nupkg.xml +++ b/api/Avalonia.Skia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -169,4 +169,4 @@ baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll - \ No newline at end of file + diff --git a/api/Avalonia.Win32.Interoperability.nupkg.xml b/api/Avalonia.Win32.Interoperability.nupkg.xml index 33fc2ac062..3672bb9b99 100644 --- a/api/Avalonia.Win32.Interoperability.nupkg.xml +++ b/api/Avalonia.Win32.Interoperability.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll - \ No newline at end of file + diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 5fe2e48f60..44617ccf64 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -163,6 +163,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Media.TextFormatting.TextRange + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0001 T:Avalonia.Platform.IGeometryContext2 @@ -631,6 +637,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Media.TextFormatting.TextRange + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0001 T:Avalonia.Platform.IGeometryContext2 @@ -5221,4 +5233,4 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - \ No newline at end of file + diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 1586639fbd..13fc22b3e5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -132,5 +132,71 @@ namespace Avalonia.Media.TextFormatting return Math.Min(length, text.Length); } + + /// + /// References a portion of a text buffer. + /// + private readonly record struct TextRange + { + public TextRange(int start, int length) + { + Start = start; + Length = length; + } + + /// + /// Gets the start. + /// + /// + /// The start. + /// + public int Start { get; } + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public int Length { get; } + + /// + /// Gets the end. + /// + /// + /// The end. + /// + public int End => Start + Length - 1; + + /// + /// Returns a specified number of contiguous elements from the start of the slice. + /// + /// The number of elements to return. + /// A that contains the specified number of elements from the start of this slice. + public TextRange Take(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new TextRange(Start, length); + } + + /// + /// Bypasses a specified number of elements in the slice and then returns the remaining elements. + /// + /// The number of elements to skip before returning the remaining elements. + /// A that contains the elements that occur after the specified index in this slice. + public TextRange Skip(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new TextRange(Start + length, Length - length); + } + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs deleted file mode 100644 index e8bab55aff..0000000000 --- a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// References a portion of a text buffer. - /// - public readonly record struct TextRange - { - public TextRange(int start, int length) - { - Start = start; - Length = length; - } - - /// - /// Gets the start. - /// - /// - /// The start. - /// - public int Start { get; } - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public int Length { get; } - - /// - /// Gets the end. - /// - /// - /// The end. - /// - public int End => Start + Length - 1; - - /// - /// Returns a specified number of contiguous elements from the start of the slice. - /// - /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public TextRange Take(int length) - { - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new TextRange(Start, length); - } - - /// - /// Bypasses a specified number of elements in the slice and then returns the remaining elements. - /// - /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public TextRange Skip(int length) - { - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new TextRange(Start + length, Length - length); - } - } -} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs index 7bde885502..0d2da06a05 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -1,4 +1,5 @@ -using Avalonia.Media.TextFormatting; +using System; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { @@ -14,8 +15,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting _runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; } - public static TextRange TextRange => new TextRange(0, 50); - public TextRun? GetTextRun(int textSourceIndex) { if (textSourceIndex >= 50) From 8dcfc7ebec6f27b458315b81dc110a7bb0960510 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 16 Mar 2026 08:27:34 +0000 Subject: [PATCH 22/24] Make SelectionHandleType internal (#20908) * made selection handle type internal * update apidiff --- api/Avalonia.nupkg.xml | 12 ++++++++++++ .../Primitives/SelectionHandleType.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 44617ccf64..e160bda11e 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -409,6 +409,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Primitives.SelectionHandleType + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Remote.RemoteServer @@ -883,6 +889,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Primitives.SelectionHandleType + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Remote.RemoteServer diff --git a/src/Avalonia.Controls/Primitives/SelectionHandleType.cs b/src/Avalonia.Controls/Primitives/SelectionHandleType.cs index 2e1955de26..58b2b01f97 100644 --- a/src/Avalonia.Controls/Primitives/SelectionHandleType.cs +++ b/src/Avalonia.Controls/Primitives/SelectionHandleType.cs @@ -3,7 +3,7 @@ /// /// Represents which part of the selection the TextSelectionHandle controls. /// - public enum SelectionHandleType + internal enum SelectionHandleType { /// /// The Handle controls the caret position. From 97f36b9f341ec1d873f7e05b93f9ae4390282dde Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 16 Mar 2026 13:45:58 +0500 Subject: [PATCH 23/24] Use the correct flag to determine if extra dirty rect needs to be combined with existing one (#20896) --- ...verCompositionVisual.ComputedProperties.cs | 4 +-- .../ServerCompositionVisual.DirtyInputs.cs | 2 +- .../ServerCompositionVisual.Update.cs | 6 ++--- .../Rendering/CompositorInvalidationTests.cs | 25 +++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs index ed8860e04a..e2ce331318 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs @@ -50,7 +50,7 @@ partial class ServerCompositionVisual private LtrbRect? _ownClipRect; - private bool _hasExtraDirtyRect; + private bool _needsToAddExtraDirtyRectToDirtyRegion; private LtrbRect _extraDirtyRect; public virtual LtrbRect? ComputeOwnContentBounds() => null; @@ -107,7 +107,7 @@ partial class ServerCompositionVisual _isDirtyForRender |= dirtyForRender; // If node itself is dirty for render, we don't need to keep track of extra dirty rects - _hasExtraDirtyRect = !dirtyForRender && (_hasExtraDirtyRect || additionalDirtyRegion); + _needsToAddExtraDirtyRectToDirtyRegion = !dirtyForRender && (_needsToAddExtraDirtyRectToDirtyRegion || additionalDirtyRegion); } public void RecomputeOwnProperties() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs index 8352fc70e2..35debea184 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs @@ -166,7 +166,7 @@ partial class ServerCompositionVisual protected void AddExtraDirtyRect(LtrbRect rect) { - _extraDirtyRect = _hasExtraDirtyRect ? _extraDirtyRect.Union(rect) : rect; + _extraDirtyRect = _delayPropagateHasExtraDirtyRects ? _extraDirtyRect.Union(rect) : rect; _delayPropagateHasExtraDirtyRects = true; EnqueueOwnPropertiesRecompute(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs index b8322225bd..f9b65e01e0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs @@ -56,7 +56,7 @@ internal partial class ServerCompositionVisual private bool NeedToPushBoundsAffectingProperties(ServerCompositionVisual node) { - return (node._isDirtyForRenderInSubgraph || node._hasExtraDirtyRect || node._contentChanged); + return (node._isDirtyForRenderInSubgraph || node._needsToAddExtraDirtyRectToDirtyRegion || node._contentChanged); } public void PreSubgraph(ServerCompositionVisual node, out bool visitChildren) @@ -142,7 +142,7 @@ internal partial class ServerCompositionVisual // specified before the tranform, i.e. in inner space, hence we have to pick them // up before we pop the transform from the transform stack. // - if (node._hasExtraDirtyRect) + if (node._needsToAddExtraDirtyRectToDirtyRegion) { AddToDirtyRegion(node._extraDirtyRect); } @@ -169,7 +169,7 @@ internal partial class ServerCompositionVisual node._isDirtyForRender = false; node._isDirtyForRenderInSubgraph = false; node._needsBoundingBoxUpdate = false; - node._hasExtraDirtyRect = false; + node._needsToAddExtraDirtyRectToDirtyRegion = false; node._contentChanged = false; } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs index 699f450223..ef0e01a104 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs @@ -38,6 +38,31 @@ public class CompositorInvalidationTests : CompositorTestsBase s.AssertRects(new Rect(30, 50, 20, 10)); } } + + [Fact] + public void Sibling_Controls_Should_Invalidate_Union_Rect_When_Removed() + { + using (var s = new CompositorCanvas()) + { + var control = new Border() + { + Background = Brushes.Red, Width = 20, Height = 10, + [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 10 + }; + var control2 = new Border() + { + Background = Brushes.Blue, Width = 20, Height = 10, + [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50 + }; + s.Canvas.Children.Add(control); + s.Canvas.Children.Add(control2); + s.RunJobs(); + s.Events.Rects.Clear(); + s.Canvas.Children.Remove(control); + s.Canvas.Children.Remove(control2); + s.AssertRects(new Rect(30, 10, 20, 50)); + } + } [Fact] public void Control_Should_Invalidate_Both_Own_Rects_When_Moved() From 536daf0b4c7b9e3bf0a24b93058c8157cbaf1d9b Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Mar 2026 10:19:41 +0100 Subject: [PATCH 24/24] Open FocusManager API (#20854) * Open FocusManager API * Merge some FocusManager overloads * Update API suppressions * Properly reset reused XYFocusOptions instances * Clarify Focus documentation * Improve FocusManager documentation * Update API suppressions --- api/Avalonia.nupkg.xml | 132 ++++++++++++++ .../Input/FindNextElementOptions.cs | 37 ++++ src/Avalonia.Base/Input/FocusManager.cs | 169 ++++++++---------- src/Avalonia.Base/Input/IFocusManager.cs | 63 ++++++- src/Avalonia.Base/Input/InputElement.cs | 4 +- .../Input/Navigation/XYFocusOptions.cs | 29 ++- .../PresentationSource/PresentationSource.cs | 4 +- .../Input/InputElement_Focus.cs | 2 +- .../Input/KeyboardDeviceTests.cs | 4 +- tests/Avalonia.UnitTests/TestRoot.cs | 2 +- 10 files changed, 337 insertions(+), 109 deletions(-) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index e160bda11e..dd20d0f39e 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1117,12 +1117,48 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.IFocusManager.ClearFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler @@ -2611,12 +2647,48 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.IFocusManager.ClearFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler @@ -4027,6 +4099,36 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Input.IFocusManager.FindFirstFocusableElement + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindLastFocusableElement + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType}) @@ -4315,6 +4417,36 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Input.IFocusManager.FindFirstFocusableElement + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindLastFocusableElement + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType}) diff --git a/src/Avalonia.Base/Input/FindNextElementOptions.cs b/src/Avalonia.Base/Input/FindNextElementOptions.cs index e6062daf9b..72d83ec419 100644 --- a/src/Avalonia.Base/Input/FindNextElementOptions.cs +++ b/src/Avalonia.Base/Input/FindNextElementOptions.cs @@ -6,12 +6,49 @@ using System.Threading.Tasks; namespace Avalonia.Input { + /// + /// Provides options to customize the behavior when identifying the next element to focus + /// during a navigation operation. + /// public sealed class FindNextElementOptions { + /// + /// Gets or sets the root within which the search for the next + /// focusable element will be conducted. + /// + /// + /// This property defines the boundary for focus navigation operations. It determines the root element + /// in the visual tree under which the focusable item search is performed. If not specified, the search + /// will default to the current scope. + /// public InputElement? SearchRoot { get; init; } + + /// + /// Gets or sets the rectangular region within the visual hierarchy that will be excluded + /// from consideration during focus navigation. + /// public Rect ExclusionRect { get; init; } + + /// + /// Gets or sets a rectangular region that serves as a hint for focus navigation. + /// This property specifies a rectangle, relative to the coordinate system of the search root, + /// which can be used as a preferred or prioritized target when navigating focus. + /// It can be null if no specific hint region is provided. + /// public Rect? FocusHintRectangle { get; init; } + + /// + /// Specifies an optional override for the navigation strategy used in XY focus navigation. + /// This property allows customizing the focus movement behavior when navigating between UI elements. + /// public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; } + + /// + /// Specifies whether occlusivity (overlapping of elements or obstructions) + /// should be ignored during focus navigation. When set to true, + /// the navigation logic disregards obstructions that may block a potential + /// focus target, allowing elements behind such obstructions to be considered. + /// public bool IgnoreOcclusivity { get; init; } } } diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 15b8fea77d..dc62171f48 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -4,7 +4,6 @@ using System.Linq; using Avalonia.Input.Navigation; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Input @@ -12,7 +11,6 @@ namespace Avalonia.Input /// /// Manages focus for the application. /// - [PrivateApi] public class FocusManager : IFocusManager { /// @@ -42,58 +40,51 @@ namespace Avalonia.Input RoutingStrategies.Tunnel); } + [PrivateApi] public FocusManager() { - _contentRoot = null; } - public FocusManager(IInputElement contentRoot) - { - _contentRoot = contentRoot; - } - - internal void SetContentRoot(IInputElement? contentRoot) + /// + /// Gets or sets the content root for the focus management system. + /// + [PrivateApi] + public IInputElement? ContentRoot { - _contentRoot = contentRoot; + get => _contentRoot; + set => _contentRoot = value; } private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; - private XYFocus _xyFocus = new(); - private XYFocusOptions _xYFocusOptions = new XYFocusOptions(); + private readonly XYFocus _xyFocus = new(); private IInputElement? _contentRoot; + private XYFocusOptions? _reusableFocusOptions; - /// - /// Gets the currently focused . - /// + /// public IInputElement? GetFocusedElement() => Current; - /// - /// Focuses a control. - /// - /// The control to focus. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. + /// public bool Focus( - IInputElement? control, + IInputElement? element, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { if (KeyboardDevice.Instance is not { } keyboardDevice) return false; - if (control is not null) + if (element is not null) { - if (!CanFocus(control)) + if (!CanFocus(element)) return false; - if (GetFocusScope(control) is StyledElement scope) + if (GetFocusScope(element) is StyledElement scope) { - scope.SetValue(FocusedElementProperty, control); + scope.SetValue(FocusedElementProperty, element); _focusRoot = GetFocusRoot(scope); } - keyboardDevice.SetFocusedElement(control, method, keyModifiers); + keyboardDevice.SetFocusedElement(element, method, keyModifiers); return true; } else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore && @@ -110,12 +101,7 @@ namespace Avalonia.Input } } - public void ClearFocus() - { - Focus(null); - } - - public void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent) + internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent) { if (oldParent is IInputElement parentElement && GetFocusScope(parentElement) is StyledElement scope && @@ -129,6 +115,7 @@ namespace Avalonia.Input Focus(null); } + [PrivateApi] public IInputElement? GetFocusedElement(IFocusScope scope) { return (scope as StyledElement)?.GetValue(FocusedElementProperty); @@ -138,6 +125,7 @@ namespace Avalonia.Input /// Notifies the focus manager of a change in focus scope. /// /// The new focus scope. + [PrivateApi] public void SetFocusScope(IFocusScope scope) { if (GetFocusedElement(scope) is { } focused) @@ -153,12 +141,14 @@ namespace Avalonia.Input } } + [PrivateApi] public void RemoveFocusRoot(IFocusScope scope) { if (scope == _focusRoot) - ClearFocus(); + Focus(null); } + [PrivateApi] public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; /// @@ -176,25 +166,15 @@ namespace Avalonia.Input ?? (FocusManager?)AvaloniaLocator.Current.GetService(); } - /// - /// Attempts to change focus from the element with focus to the next focusable element in the specified direction. - /// - /// The direction to traverse (in tab order). - /// true if focus moved; otherwise, false. - public bool TryMoveFocus(NavigationDirection direction) + /// + public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null) { - return FindAndSetNextFocus(direction, _xYFocusOptions); - } + ValidateDirection(direction); - /// - /// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options. - /// - /// The direction to traverse (in tab order). - /// The options to help identify the next element to receive focus with keyboard/controller/remote navigation. - /// true if focus moved; otherwise, false. - public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions options) - { - return FindAndSetNextFocus(direction, ValidateAndCreateFocusOptions(direction, options)); + var focusOptions = ToFocusOptions(options, true); + var result = FindAndSetNextFocus(direction, focusOptions); + _reusableFocusOptions = focusOptions; + return result; } /// @@ -295,10 +275,7 @@ namespace Avalonia.Input return true; } - /// - /// Retrieves the first element that can receive focus. - /// - /// The first focusable element. + /// public IInputElement? FindFirstFocusableElement() { var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement; @@ -317,10 +294,7 @@ namespace Avalonia.Input return GetFirstFocusableElement(searchScope); } - /// - /// Retrieves the last element that can receive focus. - /// - /// The last focusable element. + /// public IInputElement? FindLastFocusableElement() { var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement; @@ -339,52 +313,59 @@ namespace Avalonia.Input return GetFocusManager(searchScope)?.GetLastFocusableElement(searchScope); } - /// - /// Retrieves the element that should receive focus based on the specified navigation direction. - /// - /// - /// - public IInputElement? FindNextElement(NavigationDirection direction) + /// + public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null) { - var xyOption = new XYFocusOptions() - { - UpdateManifold = false - }; + ValidateDirection(direction); - return FindNextFocus(direction, xyOption); + var focusOptions = ToFocusOptions(options, false); + var result = FindNextFocus(direction, focusOptions); + _reusableFocusOptions = focusOptions; + return result; } - /// - /// Retrieves the element that should receive focus based on the specified navigation direction (cannot be used with tab navigation). - /// - /// The direction that focus moves from element to element within the app UI. - /// The options to help identify the next element to receive focus with the provided navigation. - /// The next element to receive focus. - public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions options) + private static void ValidateDirection(NavigationDirection direction) { - return FindNextFocus(direction, ValidateAndCreateFocusOptions(direction, options)); + if (direction is not ( + NavigationDirection.Next or + NavigationDirection.Previous or + NavigationDirection.Up or + NavigationDirection.Down or + NavigationDirection.Left or + NavigationDirection.Right)) + { + throw new ArgumentOutOfRangeException( + nameof(direction), + direction, + $"Only {nameof(NavigationDirection.Next)}, {nameof(NavigationDirection.Previous)}, " + + $"{nameof(NavigationDirection.Up)}, {nameof(NavigationDirection.Down)}," + + $" {nameof(NavigationDirection.Left)} and {nameof(NavigationDirection.Right)} directions are supported"); + } } - private static XYFocusOptions ValidateAndCreateFocusOptions(NavigationDirection direction, FindNextElementOptions options) + private XYFocusOptions ToFocusOptions(FindNextElementOptions? options, bool updateManifold) { - if (direction is not NavigationDirection.Up - and not NavigationDirection.Down - and not NavigationDirection.Left - and not NavigationDirection.Right) + // XYFocus only uses the options and never modifies them; we can cache and reset them between calls. + var focusOptions = _reusableFocusOptions; + _reusableFocusOptions = null; + + if (focusOptions is null) + focusOptions = new XYFocusOptions(); + else + focusOptions.Reset(); + + if (options is not null) { - throw new ArgumentOutOfRangeException(nameof(direction), - $"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported"); + focusOptions.SearchRoot = options.SearchRoot; + focusOptions.ExclusionRect = options.ExclusionRect; + focusOptions.FocusHintRectangle = options.FocusHintRectangle; + focusOptions.NavigationStrategyOverride = options.NavigationStrategyOverride; + focusOptions.IgnoreOcclusivity = options.IgnoreOcclusivity; } - return new XYFocusOptions - { - UpdateManifold = false, - SearchRoot = options.SearchRoot, - ExclusionRect = options.ExclusionRect, - FocusHintRectangle = options.FocusHintRectangle, - NavigationStrategyOverride = options.NavigationStrategyOverride, - IgnoreOcclusivity = options.IgnoreOcclusivity - }; + focusOptions.UpdateManifold = updateManifold; + + return focusOptions; } internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true) diff --git a/src/Avalonia.Base/Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs index 5691172f3f..9bd1fb4239 100644 --- a/src/Avalonia.Base/Input/IFocusManager.cs +++ b/src/Avalonia.Base/Input/IFocusManager.cs @@ -14,9 +14,66 @@ namespace Avalonia.Input IInputElement? GetFocusedElement(); /// - /// Clears currently focused element. + /// Focuses a control. /// - [Unstable("This API might be removed in 11.x minor updates. Please consider focusing another element instead of removing focus at all for better UX.")] - void ClearFocus(); + /// The control to focus. + /// The method by which focus was changed. + /// Any key modifiers active at the time of focus. + /// true if the focus moved to a control; otherwise, false. + /// + /// If is null, this method tries to clear the focus. However, it is not advised. + /// For a better user experience, focus should be moved to another element when possible. + /// + /// When this method return true, it is not guaranteed that the focus has been moved + /// to . The focus might have been redirected to another element. + /// + bool Focus( + IInputElement? element, + NavigationMethod method = NavigationMethod.Unspecified, + KeyModifiers keyModifiers = KeyModifiers.None); + + /// + /// Attempts to change focus from the element with focus to the next focusable element in the specified direction. + /// + /// + /// The direction that focus moves from element to element. + /// Must be one of , , + /// , , + /// and . + /// + /// + /// The options to help identify the next element to receive focus. + /// They only apply to directional navigation. + /// + /// true if focus moved; otherwise, false. + bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null); + + /// + /// Retrieves the first element that can receive focus. + /// + /// The first focusable element. + IInputElement? FindFirstFocusableElement(); + + /// + /// Retrieves the last element that can receive focus. + /// + /// The last focusable element. + IInputElement? FindLastFocusableElement(); + + /// + /// Retrieves the element that should receive focus based on the specified navigation direction. + /// + /// + /// The direction that focus moves from element to element. + /// Must be one of , , + /// , , + /// and . + /// + /// + /// The options to help identify the next element to receive focus. + /// They only apply to directional navigation. + /// + /// The next element to receive focus, if any. + IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null); } } diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 1beccf341e..e908e818e8 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -523,7 +523,7 @@ namespace Avalonia.Input if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager && Equals(focusManager.GetFocusedElement(), this)) { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } @@ -995,7 +995,7 @@ namespace Avalonia.Input } else { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } diff --git a/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs b/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs index 4bfcb22502..8e4c847aa9 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs @@ -1,17 +1,38 @@ namespace Avalonia.Input.Navigation; -internal class XYFocusOptions +internal sealed class XYFocusOptions { public InputElement? SearchRoot { get; set; } public Rect ExclusionRect { get; set; } public Rect? FocusHintRectangle { get; set; } public Rect? FocusedElementBounds { get; set; } public XYFocusNavigationStrategy? NavigationStrategyOverride { get; set; } - public bool IgnoreClipping { get; set; } = true; + public bool IgnoreClipping { get; set; } public bool IgnoreCone { get; set; } public KeyDeviceType? KeyDeviceType { get; set; } - public bool ConsiderEngagement { get; set; } = true; - public bool UpdateManifold { get; set; } = true; + public bool ConsiderEngagement { get; set; } + public bool UpdateManifold { get; set; } public bool UpdateManifoldsFromFocusHintRect { get; set; } public bool IgnoreOcclusivity { get; set; } + + public XYFocusOptions() + { + Reset(); + } + + internal void Reset() + { + SearchRoot = null; + ExclusionRect = default; + FocusHintRectangle = null; + FocusedElementBounds = null; + NavigationStrategyOverride = null; + IgnoreClipping = true; + IgnoreCone = false; + KeyDeviceType = null; + ConsiderEngagement = true; + UpdateManifold = true; + UpdateManifoldsFromFocusHintRect = false; + IgnoreOcclusivity = false; + } } diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs index c98a380640..9917f82c93 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs @@ -61,7 +61,7 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi field?.SetPresentationSourceForRootVisual(this); Renderer.CompositionTarget.Root = field?.CompositionVisual; - FocusManager.SetContentRoot(value as IInputElement); + FocusManager.ContentRoot = value; } } @@ -152,4 +152,4 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi } return null; } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index 7755eb80cf..cdb4588fff 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -577,7 +577,7 @@ namespace Avalonia.Base.UnitTests.Input }; target.Focus(); - root.FocusManager.ClearFocus(); + root.FocusManager.Focus(null); Assert.Null(root.FocusManager.GetFocusedElement()); } diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs index d11872ba6a..b1446d961f 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs @@ -15,7 +15,7 @@ namespace Avalonia.Base.UnitTests.Input using (UnitTestApplication.Start(TestServices.FocusableWindow)) { var window = new Window(); - window.FocusManager.ClearFocus(); + window.FocusManager.Focus(null); int raised = 0; window.KeyDown += (sender, ev) => { @@ -71,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Input using (UnitTestApplication.Start(TestServices.FocusableWindow)) { var window = new Window(); - window.FocusManager.ClearFocus(); + window.FocusManager.Focus(null); int raised = 0; window.TextInput += (sender, ev) => { diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index ed91463346..4400d77267 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -74,7 +74,7 @@ namespace Avalonia.UnitTests IRenderer IPresentationSource.Renderer => Renderer; IHitTester IPresentationSource.HitTester => HitTester; - public IFocusManager FocusManager => _focusManager ??= new FocusManager(this); + public IFocusManager FocusManager => _focusManager ??= new FocusManager { ContentRoot = this }; public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService(); public IInputElement? PointerOverElement { get; set; }