From 844542862c899653589be278e8f9bf397c0e5104 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 16 Feb 2026 00:16:54 +0500 Subject: [PATCH] Move ILayoutRoot to PresentationSource # Conflicts: # tests/Avalonia.Controls.UnitTests/TabControlTests.cs --- src/Avalonia.Base/Layout/ILayoutManager.cs | 2 +- src/Avalonia.Base/Layout/ILayoutRoot.cs | 6 +- src/Avalonia.Base/Layout/LayoutHelper.cs | 3 +- src/Avalonia.Base/Layout/LayoutManager.cs | 20 +++-- src/Avalonia.Base/Layout/Layoutable.cs | 20 ++--- .../Rendering/IPresentationSource.cs | 3 + .../VisualTree/VisualExtensions.cs | 9 +++ .../DateTimePickers/DatePicker.cs | 3 +- .../DateTimePickers/TimePicker.cs | 3 +- src/Avalonia.Controls/Grid.cs | 2 +- src/Avalonia.Controls/GridSplitter.cs | 3 +- .../PresentationSource.Layout.cs | 69 ++++++++++++++++ .../PresentationSource/PresentationSource.cs | 7 +- src/Avalonia.Controls/TopLevel.cs | 78 +------------------ src/Avalonia.Controls/TreeView.cs | 2 +- .../VirtualizingStackPanel.cs | 2 +- src/Avalonia.Controls/Window.cs | 2 +- .../ButtonTests.cs | 11 +-- .../CarouselTests.cs | 2 +- .../ItemsControlTests.cs | 2 +- .../ListBoxTests.cs | 2 +- .../MaskedTextBoxTests.cs | 11 +-- .../Primitives/SelectingItemsControlTests.cs | 2 +- .../SelectingItemsControlTests_Multiple.cs | 2 +- .../TabControlTests.cs | 12 +-- .../TextBoxTests.cs | 11 +-- .../TopLevelTests.cs | 42 ++++------ .../TransitioningContentControlTests.cs | 2 +- .../TreeViewTests.cs | 2 +- .../VirtualizingCarouselPanelTests.cs | 2 +- .../VirtualizingStackPanelTests.cs | 3 +- tests/Avalonia.RenderTests/TestRenderRoot.cs | 15 +++- tests/Avalonia.UnitTests/TestRoot.cs | 5 ++ 33 files changed, 171 insertions(+), 189 deletions(-) create mode 100644 src/Avalonia.Controls/PresentationSource/PresentationSource.Layout.cs diff --git a/src/Avalonia.Base/Layout/ILayoutManager.cs b/src/Avalonia.Base/Layout/ILayoutManager.cs index 588ce97ba5..560d0aab00 100644 --- a/src/Avalonia.Base/Layout/ILayoutManager.cs +++ b/src/Avalonia.Base/Layout/ILayoutManager.cs @@ -6,7 +6,7 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - internal interface ILayoutManager : IDisposable + public interface ILayoutManager : IDisposable { /// /// Raised when the layout manager completes a layout pass. diff --git a/src/Avalonia.Base/Layout/ILayoutRoot.cs b/src/Avalonia.Base/Layout/ILayoutRoot.cs index 0bfb10ea36..8624194c42 100644 --- a/src/Avalonia.Base/Layout/ILayoutRoot.cs +++ b/src/Avalonia.Base/Layout/ILayoutRoot.cs @@ -10,11 +10,13 @@ namespace Avalonia.Layout /// /// The scaling factor to use in layout. /// - double LayoutScaling { get; } + public double LayoutScaling { get; } /// /// Associated instance of layout manager /// - internal ILayoutManager LayoutManager { get; } + public ILayoutManager LayoutManager { get; } + + public Layoutable RootVisual { get; } } } diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index a342c654f9..84f1fc4bcc 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -140,8 +140,7 @@ namespace Avalonia.Layout /// /// The control. /// Thrown when control has no root or returned layout scaling is invalid. - public static double GetLayoutScale(Layoutable control) - => control.VisualRoot is ILayoutRoot layoutRoot ? layoutRoot.LayoutScaling : 1.0; + public static double GetLayoutScale(Layoutable control) => control.GetLayoutRoot()?.LayoutScaling ?? 1.0; /// /// Rounds a size to integer values for layout purposes, compensating for high DPI screen diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 6199b38b5c..fa82ff1d31 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -9,6 +9,7 @@ using Avalonia.Metadata; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.Utilities; +using Avalonia.VisualTree; #nullable enable @@ -20,7 +21,7 @@ namespace Avalonia.Layout internal class LayoutManager : ILayoutManager, IDisposable { private const int MaxPasses = 10; - private readonly Layoutable _owner; + private readonly ILayoutRoot _owner; private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); private readonly List _toArrangeAfterMeasure = new(); @@ -33,7 +34,7 @@ namespace Avalonia.Layout public LayoutManager(ILayoutRoot owner) { - _owner = owner as Layoutable ?? throw new ArgumentNullException(nameof(owner)); + _owner = owner; _invokeOnRender = ExecuteQueuedLayoutPass; } @@ -62,7 +63,7 @@ namespace Avalonia.Layout #endif } - if (control.VisualRoot != _owner) + if (control.GetLayoutRoot() != _owner) { throw new ArgumentException("Attempt to call InvalidateMeasure on wrong LayoutManager."); } @@ -92,7 +93,7 @@ namespace Avalonia.Layout #endif } - if (control.VisualRoot != _owner) + if (control.GetLayoutRoot() != _owner) { throw new ArgumentException("Attempt to call InvalidateArrange on wrong LayoutManager."); } @@ -187,9 +188,12 @@ namespace Avalonia.Layout try { + if (_owner?.RootVisual == null) + return; + var root = _owner.RootVisual; _running = true; - Measure(_owner); - Arrange(_owner); + Measure(root); + Arrange(root); } finally { @@ -299,7 +303,7 @@ namespace Avalonia.Layout // control to be removed. if (!control.IsMeasureValid) { - if (control is ILayoutRoot root) + if (control.GetLayoutRoot()?.RootVisual == control) { control.Measure(Size.Infinity); } @@ -328,7 +332,7 @@ namespace Avalonia.Layout if (!control.IsArrangeValid) { - if (control is ILayoutRoot root) + if (control.GetLayoutRoot()?.RootVisual == control) control.Arrange(new Rect(control.DesiredSize)); else if (control.PreviousArrange != null) { diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 39a8096a49..fedea332b6 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -168,7 +168,7 @@ namespace Avalonia.Layout { add { - if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r && !_isAttachingToVisualTree) + if (_effectiveViewportChanged is null && this.GetLayoutRoot() is {} r && !_isAttachingToVisualTree) { r.LayoutManager.RegisterEffectiveViewportListener(this); } @@ -180,7 +180,7 @@ namespace Avalonia.Layout { _effectiveViewportChanged -= value; - if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + if (_effectiveViewportChanged is null && this.GetLayoutRoot() is {} r) { r.LayoutManager.UnregisterEffectiveViewportListener(this); } @@ -194,7 +194,7 @@ namespace Avalonia.Layout { add { - if (_layoutUpdated is null && VisualRoot is ILayoutRoot r && !_isAttachingToVisualTree) + if (_layoutUpdated is null && this.GetLayoutRoot() is {} r && !_isAttachingToVisualTree) { r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; } @@ -206,7 +206,7 @@ namespace Avalonia.Layout { _layoutUpdated -= value; - if (_layoutUpdated is null && VisualRoot is ILayoutRoot r) + if (_layoutUpdated is null && this.GetLayoutRoot() is {} r) { r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; } @@ -221,7 +221,7 @@ namespace Avalonia.Layout /// schedule layout passes itself. /// - public void UpdateLayout() => (this.GetPresentationSource()?.RootVisual as ILayoutRoot)?.LayoutManager?.ExecuteLayoutPass(); + public void UpdateLayout() => this.GetLayoutManager()?.ExecuteLayoutPass(); /// /// Gets or sets the width of the element. @@ -449,7 +449,7 @@ namespace Avalonia.Layout if (IsAttachedToVisualTree) { - (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); + this.GetLayoutManager()?.InvalidateMeasure(this); InvalidateVisual(); } OnMeasureInvalidated(); @@ -466,7 +466,7 @@ namespace Avalonia.Layout Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated arrange"); IsArrangeValid = false; - (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); + this.GetLayoutManager()?.InvalidateArrange(this); InvalidateVisual(); } } @@ -794,7 +794,7 @@ namespace Avalonia.Layout _isAttachingToVisualTree = false; } - if (e.Root is ILayoutRoot r) + if (this.GetLayoutRoot() is {} r) { if (_layoutUpdated is object) { @@ -810,7 +810,7 @@ namespace Avalonia.Layout protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - if (e.Root is ILayoutRoot r) + if (this.GetLayoutRoot() is {} r) { if (_layoutUpdated is object) { @@ -853,7 +853,7 @@ namespace Avalonia.Layout // they will need to be registered with the layout manager now that they // are again effectively visible. If IsEffectivelyVisible becomes an observable // property then we can piggy-pack on that; for the moment we do this manually. - if (VisualRoot is ILayoutRoot layoutRoot) + if (this.GetLayoutRoot() is {} layoutRoot) { var count = VisualChildren.Count; diff --git a/src/Avalonia.Base/Rendering/IPresentationSource.cs b/src/Avalonia.Base/Rendering/IPresentationSource.cs index 8e36ba0c4d..27e0ead8f3 100644 --- a/src/Avalonia.Base/Rendering/IPresentationSource.cs +++ b/src/Avalonia.Base/Rendering/IPresentationSource.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Platform; namespace Avalonia.Rendering; @@ -13,6 +14,8 @@ public interface IPresentationSource internal IHitTester HitTester { get; } internal IInputRoot InputRoot { get; } + + internal ILayoutRoot LayoutRoot { get; } public Visual? RootVisual { get; } diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index 19c5c2b65a..b97c15c4df 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Utilities; @@ -464,6 +465,14 @@ namespace Avalonia.VisualTree // TODO: Verify all usages, this is no longer necessary a TopLevel internal static Visual? GetVisualRoot(this Visual visual) => visual.PresentationSource?.RootVisual; + internal static ILayoutRoot? GetLayoutRoot(this Visual visual) => visual.PresentationSource?.LayoutRoot; + + /// + /// Gets the layout manager for the visual's presentation source, or null if the visual is not attached to a visual root. + /// + public static ILayoutManager? GetLayoutManager(this Visual visual) => + visual.PresentationSource?.LayoutRoot.LayoutManager; + /// /// Attempts to obtain platform settings from the visual's root. /// This will return null if the visual is not attached to a visual root. diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 2f88e662a2..0704d1325d 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -8,6 +8,7 @@ using Avalonia.Layout; using System; using System.Collections.Generic; using System.Globalization; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -406,7 +407,7 @@ namespace Avalonia.Controls // Overlay popup hosts won't get measured until the next layout pass, but we need the // template to be applied to `_presenter` now. Detect this case and force a layout pass. if (!_presenter.IsMeasureValid) - (VisualRoot as ILayoutRoot)?.LayoutManager?.ExecuteInitialLayoutPass(); + this.GetLayoutManager()?.ExecuteInitialLayoutPass(); var deltaY = _presenter.GetOffsetForPopup(); diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index ca5c43463d..95d9e82782 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -7,6 +7,7 @@ using System; using System.Globalization; using Avalonia.Controls.Utils; using Avalonia.Automation.Peers; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -380,7 +381,7 @@ namespace Avalonia.Controls // Overlay popup hosts won't get measured until the next layout pass, but we need the // template to be applied to `_presenter` now. Detect this case and force a layout pass. if (!_presenter.IsMeasureValid) - (VisualRoot as ILayoutRoot)?.LayoutManager?.ExecuteInitialLayoutPass(); + this.GetLayoutManager()?.ExecuteInitialLayoutPass(); var deltaY = _presenter.GetOffsetForPopup(); diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index df70cdf6d0..d181328134 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -2117,7 +2117,7 @@ namespace Avalonia.Controls { // DpiScale dpiScale = GetDpi(); // double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; - var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; + var dpi = this.GetLayoutRoot()?.LayoutScaling ?? 1.0; double[] roundingErrors = RoundingErrors; double roundedTakenSize = 0; diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index ba857687f6..df502207c6 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -13,6 +13,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Utilities; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -226,7 +227,7 @@ namespace Avalonia.Controls ResizeDirection = resizeDirection, SplitterLength = Math.Min(Bounds.Width, Bounds.Height), ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection), - Scaling = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1, + Scaling = this.GetLayoutRoot()?.LayoutScaling ?? 1, }; // Store the rows and columns to resize on drag events. diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.Layout.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.Layout.cs new file mode 100644 index 0000000000..156c76cc6f --- /dev/null +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.Layout.cs @@ -0,0 +1,69 @@ +using System; +using System.ComponentModel; +using Avalonia.Layout; +using Avalonia.Rendering; + +namespace Avalonia.Controls; + +internal partial class PresentationSource : ILayoutRoot +{ + private LayoutDiagnosticBridge? _layoutDiagnosticBridge; + public double LayoutScaling => RenderScaling; + public ILayoutManager LayoutManager { get; } + ILayoutRoot IPresentationSource.LayoutRoot => this; + Layoutable ILayoutRoot.RootVisual => RootVisual; + + private ILayoutManager CreateLayoutManager() + { + var manager = new LayoutManager(this); + _layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, manager); + _layoutDiagnosticBridge.SetupBridge(); + return manager; + } + + + /// + /// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes. + /// + private sealed class LayoutDiagnosticBridge : IDisposable + { + private readonly RendererDiagnostics _diagnostics; + private readonly LayoutManager _layoutManager; + private bool _isHandling; + + public LayoutDiagnosticBridge(RendererDiagnostics diagnostics, LayoutManager layoutManager) + { + _diagnostics = diagnostics; + _layoutManager = layoutManager; + + diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged; + } + + public void SetupBridge() + { + var needsHandling = (_diagnostics.DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0; + if (needsHandling != _isHandling) + { + _isHandling = needsHandling; + _layoutManager.LayoutPassTimed = needsHandling + ? timing => _diagnostics.LastLayoutPassTiming = timing + : null; + } + } + + private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(RendererDiagnostics.DebugOverlays)) + { + SetupBridge(); + } + } + + public void Dispose() + { + _diagnostics.PropertyChanged -= OnDiagnosticsPropertyChanged; + _layoutManager.LayoutPassTimed = null; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs index b2c2789347..9afc70b113 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Input.TextInput; +using Avalonia.Layout; using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Rendering; @@ -37,8 +38,9 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi Renderer = new CompositingRenderer(this, PlatformImpl.Compositor, () => PlatformImpl.Surfaces ?? []); Renderer.SceneInvalidated += SceneInvalidated; - RootVisual = rootVisual; + LayoutManager = CreateLayoutManager(); + RootVisual = rootVisual; } // In WPF it's a Visual and it's nullable. For now we have it as non-nullable InputElement since @@ -81,6 +83,9 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi public void Dispose() { + _layoutDiagnosticBridge?.Dispose(); + _layoutDiagnosticBridge = null; + LayoutManager.Dispose(); Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index e2625a489f..829de2a2ee 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -39,7 +39,6 @@ namespace Avalonia.Controls /// [TemplatePart("PART_TransparencyFallback", typeof(Border))] public abstract class TopLevel : ContentControl, - ILayoutRoot, ICloseable, IStyleHost, ILogicalRoot @@ -126,12 +125,10 @@ namespace Avalonia.Controls private Size _clientSize; private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; - private ILayoutManager? _layoutManager; private Border? _transparencyFallbackBorder; private TargetWeakEventSubscriber? _resourcesChangesSubscriber; private IStorageProvider? _storageProvider; private Screens? _screens; - private LayoutDiagnosticBridge? _layoutDiagnosticBridge; private PresentationSource _source; internal PresentationSource PresentationSource => _source; internal IInputRoot InputRoot => _source; @@ -384,26 +381,7 @@ namespace Avalonia.Controls remove => RemoveHandler(BackRequestedEvent, value); } - internal ILayoutManager LayoutManager - { - get - { - if (_layoutManager is null) - { - _layoutManager = CreateLayoutManager(); - - if (_layoutManager is LayoutManager typedLayoutManager) - { - _layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, typedLayoutManager); - _layoutDiagnosticBridge.SetupBridge(); - } - } - - return _layoutManager; - } - } - - ILayoutManager ILayoutRoot.LayoutManager => LayoutManager; + internal ILayoutManager LayoutManager => _source.LayoutManager; /// /// Gets the platform-specific window implementation. @@ -504,8 +482,6 @@ namespace Avalonia.Controls return control.GetValue(AutoSafeAreaPaddingProperty); } - double ILayoutRoot.LayoutScaling => _scaling; - /// public double RenderScaling => _scaling; @@ -641,11 +617,6 @@ namespace Avalonia.Controls } } - /// - /// Creates the layout manager for this . - /// - private protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); - /// /// Handles a paint notification from . /// @@ -682,9 +653,6 @@ namespace Avalonia.Controls _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged; } - - _layoutDiagnosticBridge?.Dispose(); - _layoutDiagnosticBridge = null; _backGestureSubscription?.Dispose(); var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); @@ -863,49 +831,5 @@ namespace Avalonia.Controls return scaling; } - - /// - /// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes. - /// - private sealed class LayoutDiagnosticBridge : IDisposable - { - private readonly RendererDiagnostics _diagnostics; - private readonly LayoutManager _layoutManager; - private bool _isHandling; - - public LayoutDiagnosticBridge(RendererDiagnostics diagnostics, LayoutManager layoutManager) - { - _diagnostics = diagnostics; - _layoutManager = layoutManager; - - diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged; - } - - public void SetupBridge() - { - var needsHandling = (_diagnostics.DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0; - if (needsHandling != _isHandling) - { - _isHandling = needsHandling; - _layoutManager.LayoutPassTimed = needsHandling - ? timing => _diagnostics.LastLayoutPassTiming = timing - : null; - } - } - - private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(RendererDiagnostics.DebugOverlays)) - { - SetupBridge(); - } - } - - public void Dispose() - { - _diagnostics.PropertyChanged -= OnDiagnosticsPropertyChanged; - _layoutManager.LayoutPassTimed = null; - } - } } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 068ffbcb66..033be87c7f 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -168,7 +168,7 @@ namespace Avalonia.Controls item.IsExpanded = true; if (item.Presenter?.Panel is null) - (this.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + this.GetLayoutManager()?.ExecuteLayoutPass(); if (item.Presenter?.Panel is { } panel) { diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index e883bb533b..7e79026c60 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -459,7 +459,7 @@ namespace Avalonia.Controls element.BringIntoView(); return element; } - else if (this.GetVisualRoot() is ILayoutRoot root) + else if (this.GetLayoutRoot() is {} root) { // Create and measure the element to be brought into view. Store it in a field so that // it can be re-used in the layout pass. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 65ac2897cc..accda02fc6 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls /// /// A top-level window. /// - public class Window : WindowBase, IFocusScope, ILayoutRoot + public class Window : WindowBase, IFocusScope { private static readonly Lazy s_defaultIcon = new(LoadDefaultIcon); private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>(); diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 661791a6e4..20065bac60 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -759,18 +759,9 @@ namespace Avalonia.Controls.UnitTests } } - private class TestTopLevel : TopLevel + private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl) { - private readonly ILayoutManager _layoutManager; - public bool IsClosed { get; private set; } - public TestTopLevel(ITopLevelImpl impl, ILayoutManager? layoutManager = null) - : base(impl) - { - _layoutManager = layoutManager ?? new LayoutManager(this); - } - - private protected override ILayoutManager CreateLayoutManager() => _layoutManager; } } } diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 85ba254b5d..ab93686966 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -336,7 +336,7 @@ namespace Avalonia.Controls.UnitTests private static void Layout(Carousel target) { - ((ILayoutRoot)target.GetVisualRoot()!).LayoutManager.ExecuteLayoutPass(); + target.GetLayoutManager()?.ExecuteLayoutPass(); } private static IControlTemplate CarouselTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 85d366953f..b4752c5186 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -1218,7 +1218,7 @@ namespace Avalonia.Controls.UnitTests private static void Layout(Control c) { - (c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + c.GetLayoutManager()?.ExecuteLayoutPass(); } private static ContentPresenter GetContainer(ItemsControl target, int index = 0) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index c76369ef0b..2fa6a47d27 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -642,7 +642,7 @@ namespace Avalonia.Controls.UnitTests private static void Layout(Control c) { - ((ILayoutRoot)c.GetVisualRoot()!).LayoutManager.ExecuteLayoutPass(); + c.GetLayoutManager()?.ExecuteLayoutPass(); } private class Item diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 4ff22cf402..cddac813f8 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -1002,17 +1002,8 @@ namespace Avalonia.Controls.UnitTests } } - private class TestTopLevel : TopLevel + private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl) { - private readonly ILayoutManager _layoutManager; - - public TestTopLevel(ITopLevelImpl impl, ILayoutManager? layoutManager = null) - : base(impl) - { - _layoutManager = layoutManager ?? new LayoutManager(this); - } - - private protected override ILayoutManager CreateLayoutManager() => _layoutManager; } private static Mock CreateMockTopLevelImpl() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 814bf00344..3c36b7ff74 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -2555,7 +2555,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private static void Layout(Control c) { - ((ILayoutRoot)c.GetVisualRoot()!).LayoutManager.ExecuteLayoutPass(); + c.GetLayoutManager()?.ExecuteLayoutPass(); } private static FuncControlTemplate Template() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index f0f4fe05ae..2b9d7a940f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1341,7 +1341,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private static void Layout(Control c) { - (c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + c.GetLayoutManager()?.ExecuteLayoutPass(); } public static IDisposable Start() diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index a4cdf592cc..0e171474fe 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -883,18 +883,8 @@ namespace Avalonia.Controls.UnitTests }; } - private class TestTopLevel : TopLevel + private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl) { - private readonly ILayoutManager _layoutManager; - public bool IsClosed { get; private set; } - - public TestTopLevel(ITopLevelImpl impl, ILayoutManager? layoutManager = null) - : base(impl) - { - _layoutManager = layoutManager ?? new LayoutManager(this); - } - - private protected override ILayoutManager CreateLayoutManager() => _layoutManager; } private static void Prepare(TabControl target) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 31b8fe45e8..fa578a7afe 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -2284,17 +2284,8 @@ namespace Avalonia.Controls.UnitTests } } - private class TestTopLevel : TopLevel + private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl) { - private readonly ILayoutManager _layoutManager; - - public TestTopLevel(ITopLevelImpl impl, ILayoutManager? layoutManager = null) - : base(impl) - { - _layoutManager = layoutManager ?? new LayoutManager(this); - } - - private protected override ILayoutManager CreateLayoutManager() => _layoutManager; } private static Mock CreateMockTopLevelImpl() diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index c48e45bc4e..9614ab8df9 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -82,11 +82,11 @@ namespace Avalonia.Controls.UnitTests { var impl = CreateMockTopLevelImpl(); - var target = new TestTopLevel(impl.Object, Mock.Of()); - + var target = new TestTopLevel(impl.Object); + // The layout pass should be scheduled by the derived class. - var layoutManagerMock = Mock.Get(target.LayoutManager); - layoutManagerMock.Verify(x => x.ExecuteLayoutPass(), Times.Never); + Assert.Equal(0, target.Measured); + Assert.Equal(0, target.Arranged); } } @@ -254,22 +254,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Close_Should_Dispose_LayoutManager() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = CreateMockTopLevelImpl(true); - - var layoutManager = new Mock(); - var target = new TestTopLevel(impl.Object, layoutManager.Object); - - impl.Object.Closed!(); - - layoutManager.Verify(x => x.Dispose()); - } - } - [Fact] public void Reacts_To_Changes_In_Global_Styles() { @@ -329,18 +313,20 @@ namespace Avalonia.Controls.UnitTests return topLevel; } - private class TestTopLevel : TopLevel + private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl) { - private readonly ILayoutManager _layoutManager; - public bool IsClosed { get; private set; } - - public TestTopLevel(ITopLevelImpl impl, ILayoutManager? layoutManager = null) - : base(impl) + public int Measured, Arranged; + protected override Size MeasureCore(Size availableSize) { - _layoutManager = layoutManager ?? new LayoutManager(this); + Measured++; + return base.MeasureCore(availableSize); } - private protected override ILayoutManager CreateLayoutManager() => _layoutManager; + protected override void ArrangeCore(Rect finalRect) + { + Arranged++; + base.ArrangeCore(finalRect); + } } } } diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 8db2702e35..b84fef1fb2 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -377,7 +377,7 @@ namespace Avalonia.Controls.UnitTests private void Layout(Control c) { - (c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + c.GetLayoutManager()?.ExecuteLayoutPass(); } private class TestTransition : IPageTransition diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index bf35bc117d..1d678fa4f4 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1781,7 +1781,7 @@ namespace Avalonia.Controls.UnitTests private void Layout(Control c) { - (c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + c.GetLayoutManager()?.ExecuteLayoutPass(); } private void ClickContainer(Control container, KeyModifiers modifiers) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs index 4c0ec8436a..cc506dd7a9 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs @@ -344,6 +344,6 @@ namespace Avalonia.Controls.UnitTests }); } - private static void Layout(Control c) => ((ILayoutRoot)c.GetVisualRoot()!).LayoutManager.ExecuteLayoutPass(); + private static void Layout(Control c) => c.GetLayoutManager()?.ExecuteLayoutPass(); } } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 18c53efae7..0643e0758f 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -2352,8 +2352,7 @@ namespace Avalonia.Controls.UnitTests private static void Layout(Control target) { - var root = (ILayoutRoot?)target.GetVisualRoot(); - root?.LayoutManager.ExecuteLayoutPass(); + target.GetLayoutManager()?.ExecuteLayoutPass(); } private static IControlTemplate ListBoxItemTemplate() diff --git a/tests/Avalonia.RenderTests/TestRenderRoot.cs b/tests/Avalonia.RenderTests/TestRenderRoot.cs index 51eea3bd21..d6e88d174f 100644 --- a/tests/Avalonia.RenderTests/TestRenderRoot.cs +++ b/tests/Avalonia.RenderTests/TestRenderRoot.cs @@ -5,13 +5,14 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.TextInput; +using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering.Composition; namespace Avalonia.Skia.RenderTests { - public class TestRenderRoot : Decorator, IPresentationSource, IInputRoot + public class TestRenderRoot : Decorator, IPresentationSource, IInputRoot, ILayoutRoot { private readonly IRenderTarget _renderTarget; public Size ClientSize { get; private set; } @@ -19,6 +20,15 @@ namespace Avalonia.Skia.RenderTests IRenderer IPresentationSource.Renderer => Renderer; IHitTester IPresentationSource.HitTester => new NullHitTester(); public IInputRoot InputRoot => this; + + ILayoutRoot IPresentationSource.LayoutRoot => this; + + public double LayoutScaling => 1l; + + public ILayoutManager LayoutManager { get; } + + Layoutable ILayoutRoot.RootVisual => this; + public Visual? RootVisual => this; public double RenderScaling { get; } @@ -26,7 +36,8 @@ namespace Avalonia.Skia.RenderTests { _renderTarget = renderTarget; RenderScaling = scaling; - + LayoutManager = new LayoutManager(this); + } class NullHitTester : IHitTester diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index ede7e12c09..0237cdbdce 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -58,6 +58,11 @@ namespace Avalonia.UnitTests public double LayoutScaling { get; set; } = 1; internal ILayoutManager LayoutManager { get; set; } + + ILayoutRoot IPresentationSource.LayoutRoot => this; + + Layoutable ILayoutRoot.RootVisual => RootElement; + ILayoutManager ILayoutRoot.LayoutManager => LayoutManager; public Visual? RootVisual => this;