From ecad72a1f46029562af5e79b2644dfd87c6b659c Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Sun, 15 Sep 2024 15:09:12 +0300 Subject: [PATCH] fix: Initiate layouting fix. Not yet complete --- .../Given_GridLayouting.cs | 6 - .../Native/NativeRefreshControl.Android.cs | 87 +- .../FlipView/NativePagedView.Android.cs | 117 ++- .../ItemsStackPanel/ManagedItemsStackPanel.cs | 4 - .../ItemsWrapGridLayout.Android.cs | 3 +- .../UI/Xaml/Controls/Layouter/ILayouter.cs | 65 -- .../Controls/Layouter/Layouter.Android.cs | 82 -- .../UI/Xaml/Controls/Layouter/Layouter.cs | 862 ------------------ .../UI/Xaml/Controls/Layouter/Layouter.iOS.cs | 107 --- .../Xaml/Controls/Layouter/Layouter.macOS.cs | 119 --- .../Controls/Layouter/Layouter.unittests.cs | 39 - .../ListViewBase/ListViewBaseSource.iOS.cs | 19 +- .../VirtualizingPanelLayout.Android.cs | 4 +- .../ListViewBase/VirtualizingPanelLayout.cs | 56 +- .../NativeScrollContentPresenter.Android.cs | 171 ++-- .../Controls/TextBlock/TextBlock.Android.cs | 12 +- .../Xaml/Controls/TextBlock/TextBlock.iOS.cs | 18 +- .../UI/Xaml/FrameworkElement.Android.cs | 59 +- .../FrameworkElement.Layout.crossruntime.cs | 764 ---------------- src/Uno.UI/UI/Xaml/FrameworkElement.Layout.cs | 790 ++++++++++++++++ src/Uno.UI/UI/Xaml/FrameworkElement.cs | 133 ++- src/Uno.UI/UI/Xaml/FrameworkElement.iOS.cs | 95 +- .../UI/Xaml/FrameworkElement.iOSmacOS.cs | 12 - src/Uno.UI/UI/Xaml/IFrameworkElement.cs | 30 - ...IFrameworkElementImplementation.Android.tt | 3 +- .../UI/Xaml/ILayouterElement.Android.cs | 59 -- src/Uno.UI/UI/Xaml/ILayouterElement.cs | 125 +-- .../UI/Xaml/ILayouterElement.iOSmacOS.cs | 32 - src/Uno.UI/UI/Xaml/Internal/RootVisual.cs | 22 + src/Uno.UI/UI/Xaml/LayoutStorage.cs | 5 +- src/Uno.UI/UI/Xaml/Shapes/Shape.Android.cs | 22 +- src/Uno.UI/UI/Xaml/UIElement.Android.cs | 58 +- src/Uno.UI/UI/Xaml/UIElement.Layout.Flags.cs | 207 +++++ .../UI/Xaml/UIElement.Layout.crossruntime.cs | 498 ---------- src/Uno.UI/UI/Xaml/UIElement.Layout.cs | 657 +++++++++---- src/Uno.UI/UI/Xaml/UIElement.cs | 135 +-- src/Uno.UI/UI/Xaml/UIElement.iOS.cs | 18 +- src/Uno.UI/UI/Xaml/UIElement.macOS.cs | 12 - src/Uno.UI/UI/Xaml/UIElement.reference.cs | 4 - src/Uno.UI/UI/Xaml/UIElement.unittests.cs | 4 - 40 files changed, 1816 insertions(+), 3699 deletions(-) delete mode 100644 src/Uno.UI/UI/Xaml/Controls/Layouter/ILayouter.cs delete mode 100644 src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.Android.cs delete mode 100644 src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs delete mode 100644 src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.iOS.cs delete mode 100644 src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.macOS.cs delete mode 100644 src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.unittests.cs create mode 100644 src/Uno.UI/UI/Xaml/FrameworkElement.Layout.cs delete mode 100644 src/Uno.UI/UI/Xaml/ILayouterElement.Android.cs delete mode 100644 src/Uno.UI/UI/Xaml/ILayouterElement.iOSmacOS.cs create mode 100644 src/Uno.UI/UI/Xaml/UIElement.Layout.Flags.cs delete mode 100644 src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_GridLayouting.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_GridLayouting.cs index 55cfceb3ba52..572b953aaf25 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_GridLayouting.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_GridLayouting.cs @@ -2133,13 +2133,7 @@ public void When_One_Fixed_Size_Child_With_Margin_Right_And_Stretch() #if !WINAPPSDK private static Size GetUnclippedDesiredSize(UIElement element) { -#if UNO_REFERENCE_API return element.m_unclippedDesiredSize; -#else - var layouterElement = (ILayouterElement)element; - var layouter = (Layouter)layouterElement.Layouter; - return layouter._unclippedDesiredSize; -#endif } #endif diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/Native/NativeRefreshControl.Android.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/Native/NativeRefreshControl.Android.cs index 2ec8ebc39ffa..01cadcb9a855 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/Native/NativeRefreshControl.Android.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/Native/NativeRefreshControl.Android.cs @@ -21,7 +21,7 @@ namespace Uno.UI.Xaml.Controls; -public partial class NativeRefreshControl : SwipeRefreshLayout, IShadowChildrenProvider, DependencyObject, ILayouterElement +public partial class NativeRefreshControl : SwipeRefreshLayout, IShadowChildrenProvider, DependencyObject { // Distance in pixels a touch can wander before we think the user is scrolling // https://developer.android.com/reference/android/view/ViewConfiguration.html#getScaledTouchSlop() @@ -36,7 +36,6 @@ public partial class NativeRefreshControl : SwipeRefreshLayout, IShadowChildrenP public NativeRefreshControl() : base(ContextHelper.Current) { - _layouter = new NativeRefreshControlLayouter(this); } internal Android.Views.View Content @@ -162,93 +161,9 @@ public override bool OnTouchEvent(MotionEvent e) List IShadowChildrenProvider.ChildrenShadow => Content != null ? new List(1) { Content as View } : _emptyList; - private ILayouter _layouter; - - ILayouter ILayouterElement.Layouter => _layouter; - Size ILayouterElement.LastAvailableSize => LayoutInformation.GetAvailableSize(this); - bool ILayouterElement.IsMeasureDirty => true; - bool ILayouterElement.IsFirstMeasureDoneAndManagedElement => false; - bool ILayouterElement.StretchAffectsMeasure => true; - bool ILayouterElement.IsMeasureDirtyPathDisabled => true; - public override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { base.OnMeasure(widthMeasureSpec, heightMeasureSpec); - ((ILayouterElement)this).OnMeasureInternal(widthMeasureSpec, heightMeasureSpec); - } - - void ILayouterElement.SetMeasuredDimensionInternal(int width, int height) - { - SetMeasuredDimension(width, height); - } - - partial void OnLayoutPartial(bool changed, int left, int top, int right, int bottom) - { - var newSize = new Rect(0, 0, right - left, bottom - top).PhysicalToLogicalPixels(); - - // WARNING: The layouter must be called every time here, - // even if the size has not changed. Failing to call the layouter - // may leave the default ScrollViewer implementation place - // the child at an invalid location when the visibility changes. - - _layouter.Arrange(newSize); - } - - private class NativeRefreshControlLayouter : Layouter - { - public NativeRefreshControlLayouter(NativeRefreshControl view) : base(view) - { - } - - private NativeRefreshControl RefreshControl => Panel as NativeRefreshControl; - - protected override void MeasureChild(View child, int widthSpec, int heightSpec) - { - var childMargin = (child as FrameworkElement)?.Margin ?? Thickness.Empty; - - RefreshControl.Content?.Measure(widthSpec, heightSpec); - } - - protected override Size MeasureOverride(Size availableSize) - { - var child = RefreshControl.Content; - - var desiredChildSize = default(Size); - if (child != null) - { - var scrollSpace = availableSize; - - desiredChildSize = MeasureChild(child, scrollSpace); - - // Give opportunity to the the content to define the viewport size itself - (child as ICustomScrollInfo)?.ApplyViewport(ref desiredChildSize); - } - - return desiredChildSize; - } - - protected override Size ArrangeOverride(Size slotSize) - { - var child = RefreshControl.Content; - - if (child != null) - { - ArrangeChild(child, new Rect( - 0, - 0, - slotSize.Width, - slotSize.Height - )); - - // Give opportunity to the the content to define the viewport size itself - (child as ICustomScrollInfo)?.ApplyViewport(ref slotSize); - - } - - return slotSize; - } - - protected override string Name => Panel.Name; } private ViewGroup GetDescendantScrollable() diff --git a/src/Uno.UI/UI/Xaml/Controls/FlipView/NativePagedView.Android.cs b/src/Uno.UI/UI/Xaml/Controls/FlipView/NativePagedView.Android.cs index 59d4037c65ad..57bb8911f085 100644 --- a/src/Uno.UI/UI/Xaml/Controls/FlipView/NativePagedView.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/FlipView/NativePagedView.Android.cs @@ -25,36 +25,21 @@ private void Initialize() { InitializeBinder(); LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent); - _layouter = new NativePagedViewLayouter(this); } - private ILayouter _layouter; - - ILayouter ILayouterElement.Layouter => _layouter; - Size ILayouterElement.LastAvailableSize => LayoutInformation.GetAvailableSize(this); - bool ILayouterElement.IsMeasureDirty => true; - bool ILayouterElement.IsFirstMeasureDoneAndManagedElement => false; - bool ILayouterElement.StretchAffectsMeasure => false; - bool ILayouterElement.IsMeasureDirtyPathDisabled => true; - protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { - var measuredSize = ((ILayouterElement)this).OnMeasureInternal(widthMeasureSpec, heightMeasureSpec); - - var logicalMeasuredSize = measuredSize.PhysicalToLogicalPixels(); - - //We call ViewPager.OnMeasure here, because it creates the page views. - base.OnMeasure( - ViewHelper.SpecFromLogicalSize(logicalMeasuredSize.Width), - ViewHelper.SpecFromLogicalSize(logicalMeasuredSize.Height) - ); + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + } - IFrameworkElementHelper.OnMeasureOverride(this); + Size ILayouterElement.Measure(Size availableSize) + { + return default; // TODO } - void ILayouterElement.SetMeasuredDimensionInternal(int width, int height) + void ILayouterElement.Arrange(Rect finalRect) { - SetMeasuredDimension(width, height); + // TODO } //TODO generated code @@ -73,7 +58,7 @@ partial void OnLayoutPartial(bool changed, int left, int top, int right, int bot { _lastLayoutSize = newSize; - _layouter.Arrange(new global::Windows.Foundation.Rect(0, 0, newSize.Width, newSize.Height)); + //_layouter.Arrange(new global::Windows.Foundation.Rect(0, 0, newSize.Width, newSize.Height)); } } @@ -99,48 +84,48 @@ public override void RemoveViewAt(int index) public bool IsHeightConstrained(View requester) => (Parent as ILayoutConstraints)?.IsHeightConstrained(this) ?? false; - private class NativePagedViewLayouter : Layouter - { - - public NativePagedViewLayouter(NativePagedView view) : base(view) { } - - protected override string Name => Panel.Name; - - - protected override void MeasureChild(View view, int widthSpec, int heightSpec) - { - (Panel as NativePagedView).MeasureChild(view, widthSpec, heightSpec); - } - - protected override Size ArrangeOverride(Size finalSize) - { - //Do nothing here. FrameworkElementMixins.OnLayout calls ViewPager.OnLayout, which lays out its visible page. - return finalSize; - } - - protected override Size MeasureOverride(Size availableSize) - { - var sizeThatFits = IFrameworkElementHelper.SizeThatFits(Panel, availableSize); - - double maxChildWidth = 0f, maxChildHeight = 0f; - - //Per the link, if the NativePagedView has no fixed size in a dimension, wrap it to the size of its largest child. - http://stackoverflow.com/questions/8394681/android-i-am-unable-to-have-viewpager-wrap-content - //This might be brittle if items have varying dimensions along the unfixed axis; one (hackish) solution would be to increase OffscreenPageLimit (the number of offscreen pages that are kept). - if (double.IsPositiveInfinity(sizeThatFits.Width) || double.IsPositiveInfinity(sizeThatFits.Height)) - { - foreach (var child in this.GetChildren()) - { - var desiredChildSize = MeasureChild(child, sizeThatFits); - maxChildWidth = Math.Max(maxChildWidth, desiredChildSize.Width); - maxChildHeight = Math.Max(maxChildHeight, desiredChildSize.Height); - } - } - - return new Size( - !double.IsPositiveInfinity(sizeThatFits.Width) ? sizeThatFits.Width : maxChildWidth, - !double.IsPositiveInfinity(sizeThatFits.Height) ? sizeThatFits.Height : maxChildHeight - ); - } - } + //private class NativePagedViewLayouter : Layouter + //{ + + // public NativePagedViewLayouter(NativePagedView view) : base(view) { } + + // protected override string Name => Panel.Name; + + + // protected override void MeasureChild(View view, int widthSpec, int heightSpec) + // { + // (Panel as NativePagedView).MeasureChild(view, widthSpec, heightSpec); + // } + + // protected override Size ArrangeOverride(Size finalSize) + // { + // //Do nothing here. FrameworkElementMixins.OnLayout calls ViewPager.OnLayout, which lays out its visible page. + // return finalSize; + // } + + // protected override Size MeasureOverride(Size availableSize) + // { + // var sizeThatFits = IFrameworkElementHelper.SizeThatFits(Panel, availableSize); + + // double maxChildWidth = 0f, maxChildHeight = 0f; + + // //Per the link, if the NativePagedView has no fixed size in a dimension, wrap it to the size of its largest child. - http://stackoverflow.com/questions/8394681/android-i-am-unable-to-have-viewpager-wrap-content + // //This might be brittle if items have varying dimensions along the unfixed axis; one (hackish) solution would be to increase OffscreenPageLimit (the number of offscreen pages that are kept). + // if (double.IsPositiveInfinity(sizeThatFits.Width) || double.IsPositiveInfinity(sizeThatFits.Height)) + // { + // foreach (var child in this.GetChildren()) + // { + // var desiredChildSize = MeasureChild(child, sizeThatFits); + // maxChildWidth = Math.Max(maxChildWidth, desiredChildSize.Width); + // maxChildHeight = Math.Max(maxChildHeight, desiredChildSize.Height); + // } + // } + + // return new Size( + // !double.IsPositiveInfinity(sizeThatFits.Width) ? sizeThatFits.Width : maxChildWidth, + // !double.IsPositiveInfinity(sizeThatFits.Height) ? sizeThatFits.Height : maxChildHeight + // ); + // } + //} } } diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsStackPanel/ManagedItemsStackPanel.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsStackPanel/ManagedItemsStackPanel.cs index 95977e44dd50..f08b2d69ac9e 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ItemsStackPanel/ManagedItemsStackPanel.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ItemsStackPanel/ManagedItemsStackPanel.cs @@ -17,10 +17,6 @@ public partial class ManagedItemsStackPanel : Panel { ManagedVirtualizingPanelLayout _layout; -#if !__IOS__ - internal bool ShouldInterceptInvalidate { get; set; } -#endif - public ManagedItemsStackPanel() { if (FeatureConfiguration.ListViewBase.DefaultCacheLength.HasValue) diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsWrapGrid/ItemsWrapGridLayout.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsWrapGrid/ItemsWrapGridLayout.Android.cs index acc1017400c6..e0bddc9456d9 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ItemsWrapGrid/ItemsWrapGridLayout.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ItemsWrapGrid/ItemsWrapGridLayout.Android.cs @@ -79,7 +79,8 @@ bool isNewGroup AddView(view, direction); var slotSize = new Size(availableWidth, availableHeight).PhysicalToLogicalPixels(); - var measuredSize = _layouter.MeasureChild(view, slotSize); + //var measuredSize = _layouter.MeasureChild(view, slotSize); + var measuredSize = slotSize; // TODO var physicalMeasuredSize = measuredSize.LogicalToPhysicalPixels(); var measuredWidth = (int)physicalMeasuredSize.Width; var measuredHeight = (int)physicalMeasuredSize.Height; diff --git a/src/Uno.UI/UI/Xaml/Controls/Layouter/ILayouter.cs b/src/Uno.UI/UI/Xaml/Controls/Layouter/ILayouter.cs deleted file mode 100644 index 3174996ea4de..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Layouter/ILayouter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Windows.Foundation; - -#if __ANDROID__ -using View = Android.Views.View; -using Font = Android.Graphics.Typeface; -#elif __IOS__ -using View = UIKit.UIView; -using Color = UIKit.UIColor; -using Font = UIKit.UIFont; -#elif __MACOS__ -using View = AppKit.NSView; -using Color = AppKit.NSColor; -using Font = AppKit.NSFont; -#else -using View = Microsoft.UI.Xaml.UIElement; -#endif - -namespace Microsoft.UI.Xaml.Controls -{ - internal interface ILayouter - { - /// - /// Measures the current layout - /// - /// The available size in virtual pixels - /// The measured size in virtual pixels - Size Measure(Size availableSize); - - /// - /// Arranges the current layout. - /// - /// The maximum size to use when arranging the layout - void Arrange(Rect finalRect); - - /// - /// Measures the specified child. - /// - /// The view to measure - /// The maximum size the child can use. - /// The size the view requires. - /// - /// Provides the ability for external implementations to measure children. - /// Mainly used for compatibility with existing WPF/WinRT implementations. - /// - Size MeasureChild(View view, Size slotSize); - - /// - /// Arranges the specified view. - /// - /// The view to arrange - /// The frame available for the child. - /// - /// Provides the ability for external implementations to measure children. - /// Mainly used for compatibility with existing WPF/WinRT implementations. - /// - void ArrangeChild(View view, Rect frame); - - /// - /// Provides the desired size of the element, from the last measure phase. - /// - /// The element to get the measured with - /// The measured size - Size GetDesiredSize(View view); - } -} diff --git a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.Android.cs b/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.Android.cs deleted file mode 100644 index 9a13ccc5b082..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.Android.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Uno.Extensions; -using Uno; -using Uno.Foundation.Logging; -using Uno.Collections; -using Windows.Foundation; - -using View = Android.Views.View; -using Font = Android.Graphics.Typeface; -using System.Linq.Expressions; -using Uno.UI; - -namespace Microsoft.UI.Xaml.Controls -{ - abstract partial class Layouter - { - public static void SetMeasuredDimensions(View view, int width, int height) - { - LayouterHelper.SetMeasuredDimensions(view, new object[] { width, height }); - } - - protected Size MeasureChildOverride(View view, Size slotSize) - { - var widthSpec = ViewHelper.SpecFromLogicalSize(slotSize.Width); - var heightSpec = ViewHelper.SpecFromLogicalSize(slotSize.Height); - - var needsForceLayout = - (double.IsPositiveInfinity(slotSize.Width) || double.IsPositiveInfinity(slotSize.Height)) || - // uno12315: ensure the native measure cache is not used when measure-spec has changed since. - FeatureConfiguration.FrameworkElement.InvalidateNativeCacheOnRemeasure && ( - view.MeasuredWidth != ViewHelper.LogicalToPhysicalPixels(slotSize.Width) || - view.MeasuredHeight != ViewHelper.LogicalToPhysicalPixels(slotSize.Height) - ); - - Uno.UI.Controls.BindableView.TryFastRequestLayout(view, needsForceLayout); - - MeasureChild(view, widthSpec, heightSpec); - - var ret = Uno.UI.Controls.BindableView.GetNativeMeasuredDimensionsFast(view) - .PhysicalToLogicalPixels(); - - return ret.AtMost(slotSize); - } - - protected abstract void MeasureChild(View view, int widthSpec, int heightSpec); - - protected void ArrangeChildOverride(View view, Rect frame) - { - LogArrange(view, frame); - - var elt = view as UIElement; - var physicalFrame = frame.LogicalToPhysicalPixels(); - - try - { - elt?.SetFramePriorArrange(frame, physicalFrame); - - view.Layout( - (int)physicalFrame.Left, - (int)physicalFrame.Top, - (int)physicalFrame.Right, - (int)physicalFrame.Bottom - ); - } - finally - { - elt?.ResetFramePostArrange(); - } - } - } - - internal static partial class LayouterExtensions - { - public static IEnumerable GetChildren(this Layouter layouter) - { - return (layouter.Panel as Android.Views.ViewGroup).GetChildren(); - } - } -} diff --git a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs b/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs deleted file mode 100644 index 7618142a586b..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs +++ /dev/null @@ -1,862 +0,0 @@ -// #define LOG_LAYOUT - -#if !UNO_REFERENCE_API -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using Windows.Foundation; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Media; -using Uno; -using Uno.Extensions; -using Uno.Foundation.Logging; -using Uno.Collections; -using Uno.Diagnostics.Eventing; -using Uno.UI; -using static System.Double; -using static System.Math; -using static Uno.UI.LayoutHelper; - -#if __ANDROID__ -using Android.Views; -using View = Android.Views.View; -using Font = Android.Graphics.Typeface; -#elif __IOS__ -using View = UIKit.UIView; -using Color = UIKit.UIColor; -using Font = UIKit.UIFont; -using CoreGraphics; -#elif __MACOS__ -using View = AppKit.NSView; -using Color = AppKit.NSColor; -using Font = AppKit.NSFont; -using CoreGraphics; -#elif IS_UNIT_TESTS || __WASM__ -using View = Microsoft.UI.Xaml.UIElement; -#endif - -namespace Microsoft.UI.Xaml.Controls -{ - internal abstract partial class Layouter : ILayouter - { - private static readonly IEventProvider _trace = Tracing.Get(FrameworkElement.TraceProvider.Id); - private readonly Logger _logDebug; - - private readonly Size MaxSize = new Size(double.PositiveInfinity, double.PositiveInfinity); - - internal Size _unclippedDesiredSize; - - private readonly UIElement _elementAsUIElement; - - public IFrameworkElement Panel { get; } - - protected Layouter(IFrameworkElement element) - { - Panel = element; - _elementAsUIElement = element as UIElement; - - var log = this.Log(); - if (log.IsEnabled(LogLevel.Debug)) - { - _logDebug = log; - } - } - - /// - /// Determine the size of the panel. - /// - /// The available size, in logical pixels. - /// The size of the panel, in logical pixel. - public Size Measure(Size availableSize) - { - using var traceActivity = _trace.IsEnabled - ? _trace.WriteEventActivity( - FrameworkElement.TraceProvider.FrameworkElement_MeasureStart, - FrameworkElement.TraceProvider.FrameworkElement_MeasureStop, - new object[] { LoggingOwnerTypeName, Panel.GetDependencyObjectId() } - ) - : null; - - if (Panel.Visibility == Visibility.Collapsed) - { - // A collapsed element should not be measured at all - return default; - } - - try - { - if (_elementAsUIElement?.IsVisualTreeRoot ?? false) - { - UIElement.IsLayoutingVisualTreeRoot = true; - } - - var (minSize, maxSize) = Panel.GetMinMax(); - var marginSize = Panel.GetMarginSize(); - - // NaN values are accepted as input here, particularly when coming from - // SizeThatFits in Image or Scrollviewer. Clamp the value here as it is reused - // below for the clipping value. - var frameworkAvailableSize = availableSize - .NumberOrDefault(MaxSize); - - frameworkAvailableSize = frameworkAvailableSize - .Subtract(marginSize) - .AtLeastZero() - .AtMost(maxSize); - - // TODO: This commented code was done as part of aligning layouting on mobile platforms. - // We are reverting those changes as they require more changes, but keeping them - // commented for future reference. - //if (Panel is not ILayoutOptOut { ShouldUseMinSize: false }) - //{ - // frameworkAvailableSize = frameworkAvailableSize.AtLeast(minSize); - //} - - var desiredSize = MeasureOverride(frameworkAvailableSize); - LayoutInformation.SetAvailableSize(Panel, availableSize); - - _logDebug?.Trace($"{this}.MeasureOverride(availableSize={availableSize}); frameworkAvailableSize={frameworkAvailableSize}; desiredSize={desiredSize}"); - - if ( - double.IsNaN(desiredSize.Width) - || double.IsNaN(desiredSize.Height) - || double.IsInfinity(desiredSize.Width) - || double.IsInfinity(desiredSize.Height) - ) - { - throw new InvalidOperationException($"{this}: Invalid measured size {desiredSize}. NaN or Infinity are invalid desired size."); - } - - desiredSize = desiredSize - .AtLeast((Panel as ILayoutOptOut)?.ShouldUseMinSize == false ? Size.Empty : minSize) - .AtLeastZero(); - - if (_elementAsUIElement is not null) - { - _elementAsUIElement.EnsureLayoutStorage(); - } - - _unclippedDesiredSize = desiredSize; - - var clippedDesiredSize = desiredSize - // TODO: This commented code was done as part of aligning layouting on mobile platforms. - // We are reverting those changes as they require more changes, but keeping them - // commented for future reference. - //.AtMost(maxSize) - .AtMost(frameworkAvailableSize) // TODO: This line shouldn't be there (ie, frameworkAvailableSize should be maxSize). - .Add(marginSize) - // Making sure after adding margins that clipped DesiredSize is not bigger than the AvailableSize - // TODO: This commented code was done as part of aligning layouting on mobile platforms. - // We are reverting those changes as they require more changes, but keeping them - // commented for future reference. - //.AtMost(availableSize) - // Margin may be negative - .AtLeastZero(); - - // DesiredSize must include margins - // TODO: on UWP, it's not clipped. See test When_MinWidth_SmallerThan_AvailableSize - LayoutInformation.SetDesiredSize(Panel, clippedDesiredSize); - - // We return "clipped" desiredSize to caller... the unclipped version stays internal - return clippedDesiredSize; - } - finally - { - if (_elementAsUIElement?.IsVisualTreeRoot ?? false) - { - UIElement.IsLayoutingVisualTreeRoot = false; - } - } - } - - private static bool IsCloseReal(double a, double b) - { - var x = Math.Abs((a - b) / (b == 0d ? 1d : b)); - return x < 1.85e-3d; - } - - private static bool IsLessThanAndNotCloseTo(double a, double b) - { - return (a < b) && !IsCloseReal(a, b); - } - - /// - /// Places the children of the panel using a specific size, in logical pixels. - /// - public void Arrange(Rect finalRect) - { - using var traceActivity = _trace.IsEnabled - ? _trace.WriteEventActivity( - FrameworkElement.TraceProvider.FrameworkElement_ArrangeStart, - FrameworkElement.TraceProvider.FrameworkElement_ArrangeStop, - new object[] { LoggingOwnerTypeName, Panel.GetDependencyObjectId() } - ) - : null; - - try - { - if (_elementAsUIElement?.IsVisualTreeRoot ?? false) - { - UIElement.IsLayoutingVisualTreeRoot = true; - } - - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) - { - this.Log().DebugFormat("[{0}/{1}] Arrange({2}/{3}/{4}/{5})", LoggingOwnerTypeName, Name, GetType(), Panel.Name, finalRect, Panel.Margin); - } - - if (_elementAsUIElement is not null) - { - _elementAsUIElement.EnsureLayoutStorage(); - } - - var clippedArrangeSize = _elementAsUIElement?.ClippedFrame is Rect clip && !_elementAsUIElement.IsArrangeDirty - ? clip.Size - : finalRect.Size; - - bool allowClipToSlot; - bool needsClipToSlot; - -#if !IS_UNIT_TESTS - if (Panel is ICustomClippingElement customClippingElement) - { - // Some controls may control itself how clipping is applied - allowClipToSlot = customClippingElement.AllowClippingToLayoutSlot; - needsClipToSlot = customClippingElement.ForceClippingToLayoutSlot; - } - else -#endif - { - allowClipToSlot = true; - needsClipToSlot = false; - } - - _logDebug?.Debug($"{this}: InnerArrangeCore({finalRect}) - allowClip={allowClipToSlot}, clippedArrangeSize={clippedArrangeSize}, _unclippedDesiredSize={_unclippedDesiredSize}, forcedClipping={needsClipToSlot}"); - - var arrangeSize = finalRect - .Size - .AtLeastZero(); // 0.0,0.0 - - if (allowClipToSlot && !needsClipToSlot) - { - if (IsLessThanAndNotCloseTo(clippedArrangeSize.Width, _unclippedDesiredSize.Width)) - { - _logDebug?.Debug($"{this}: (arrangeSize.Width) {clippedArrangeSize.Width} < {_unclippedDesiredSize.Width}: NEEDS CLIPPING."); - needsClipToSlot = true; - // TODO: This commented code was done as part of aligning layouting on mobile platforms. - // We are reverting those changes as they require more changes, but keeping them - // commented for future reference. - //arrangeSize.Width = _unclippedDesiredSize.Width; - } - - if (IsLessThanAndNotCloseTo(clippedArrangeSize.Height, _unclippedDesiredSize.Height)) - { - _logDebug?.Debug($"{this}: (arrangeSize.Height) {clippedArrangeSize.Height} < {_unclippedDesiredSize.Height}: NEEDS CLIPPING."); - needsClipToSlot = true; - // TODO: This commented code was done as part of aligning layouting on mobile platforms. - // We are reverting those changes as they require more changes, but keeping them - // commented for future reference. - //arrangeSize.Height = _unclippedDesiredSize.Height; - } - } - - // Alignment==Stretch --> arrange at the slot size minus margins - // Alignment!=Stretch --> arrange at the unclippedDesiredSize - if (Panel is not Microsoft.UI.Xaml.Shapes.Shape and not ContentControl) - { - // Uno specific: Shapes arrange is relying on "wrong" layouter logic to be arranged properly - // The "Panel is not Shape" check should be removed when we're removing the legacy shape measure/arrange - // Also, it seems ContentControl is causing issues (probably related to content presenter bypass?) - if (Panel.HorizontalAlignment != HorizontalAlignment.Stretch) - { - arrangeSize.Width = _unclippedDesiredSize.Width; - } - if (Panel.VerticalAlignment != VerticalAlignment.Stretch) - { - arrangeSize.Height = _unclippedDesiredSize.Height; - } - } - - var (_, maxSize) = this.Panel.GetMinMax(); - //var marginSize = this.Panel.GetMarginSize(); - - // We have to choose max between _unclippedDesiredSize and maxSize here, because - // otherwise setting of max property could cause arrange at less then _unclippedDesiredSize. - // Clipping by Max is needed to limit stretch here - var effectiveMaxSize = Max(_unclippedDesiredSize, maxSize); - - if (allowClipToSlot) - { - if (IsLessThanAndNotCloseTo(effectiveMaxSize.Width, arrangeSize.Width)) - { - _logDebug?.Debug($"{this}: (effectiveMaxSize.Width) {effectiveMaxSize.Width} < {arrangeSize.Width}: NEEDS CLIPPING."); - needsClipToSlot = true; - arrangeSize.Width = effectiveMaxSize.Width; - } - - if (IsLessThanAndNotCloseTo(effectiveMaxSize.Height, arrangeSize.Height)) - { - _logDebug?.Debug($"{this}: (effectiveMaxSize.Height) {effectiveMaxSize.Height} < {arrangeSize.Height}: NEEDS CLIPPING."); - needsClipToSlot = true; - arrangeSize.Height = effectiveMaxSize.Height; - } - } - - var innerInkSize = ArrangeOverride(arrangeSize); - var clippedInkSize = innerInkSize.AtMost(maxSize); - - // TODO: This commented code was done as part of aligning layouting on mobile platforms. - // We are reverting those changes as they require more changes, but keeping them - // commented for future reference. - //if (IsLessThanAndNotCloseTo(clippedInkSize.Width, innerInkSize.Width) || IsLessThanAndNotCloseTo(clippedInkSize.Height, innerInkSize.Height)) - //{ - // needsClipToSlot = true; - //} - - //var clientSize = finalRect.Size - // .Subtract(marginSize) - // .AtLeastZero(); - - //var (offset, overflow) = Panel.GetAlignmentOffset(clientSize, clippedInkSize); - //var margin = Panel.Margin; - - //offset = new Point( - // offset.X + finalRect.X + margin.Left, - // offset.Y + finalRect.Y + margin.Top - //); - - //if (overflow) - //{ - // needsClipToSlot = true; - //} - - - if (_elementAsUIElement != null) - { - //_elementAsUIElement.LayoutSlotWithMarginsAndAlignments = new Rect(offset, innerInkSize); - //var layoutFrame = new Rect(offset, clippedInkSize); - - // Calculate clipped frame. - //var clippedFrameWithParentOrigin = layoutFrame.IntersectWith(finalRect.DeflateBy(margin)) ?? Rect.Empty; - - // Rebase the origin of the clipped frame to layout - //_elementAsUIElement.ClippedFrame = new Rect( - // clippedFrameWithParentOrigin.X - layoutFrame.X, - // clippedFrameWithParentOrigin.Y - layoutFrame.Y, - // clippedFrameWithParentOrigin.Width, - // clippedFrameWithParentOrigin.Height); - - _elementAsUIElement.RenderSize = clippedInkSize; // TODO: This should be innerInkSize - _elementAsUIElement.NeedsClipToSlot = needsClipToSlot; - _elementAsUIElement.ApplyClip(); - - if (Panel is FrameworkElement fe) - { - fe.OnLayoutUpdated(); - } - } - else if (Panel is IFrameworkElement_EffectiveViewport evp) - { - evp.OnLayoutUpdated(); - } - } - finally - { - if (_elementAsUIElement?.IsVisualTreeRoot ?? false) - { - UIElement.IsLayoutingVisualTreeRoot = false; - } - } - } - - /// - /// Determine the size of the panel. - /// - /// The available size, in logical pixels. - /// The size of the panel, in logical pixel. - protected abstract Size MeasureOverride(Size availableSize); - - /// - /// Places the children of the panel using a specific size, in logical pixels. - /// - /// The final panel size - protected abstract Size ArrangeOverride(Size finalSize); - - /// - /// Provides the desired size of the element, from the last measure phase. - /// - /// The element to get the measured with - /// The measured size - Size ILayouter.GetDesiredSize(View view) - => LayoutInformation.GetDesiredSize(view); - - protected Size MeasureChild(View view, Size slotSize) - { - var frameworkElement = view as IFrameworkElementInternal; - var ret = default(Size); - - // NaN values are accepted as input for MeasureOverride, but are treated as Infinity. - slotSize = slotSize.NumberOrDefault(MaxSize); - - if (frameworkElement?.Visibility == Visibility.Collapsed) - { - // By default iOS views measure to normal size, even if they're hidden. - // We want the collapsed behavior, so we return a 0,0 size instead. - - // Note: Visibility is checked in both Measure and MeasureChild, since some IFrameworkElement children may not have their own Layouter - LayoutInformation.SetDesiredSize(view, ret); - - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) - { - var viewName = frameworkElement.SelectOrDefault(f => f.Name, "NativeView"); - - this.Log().DebugFormat( - "[{0}/{1}] MeasureChild(HIDDEN/{2}/{3}/{4}/{5}) = {6}", - LoggingOwnerTypeName, - Name, - view.GetType(), - viewName, - slotSize, - frameworkElement.Margin, - ret - ); - } - - return ret; - } - - if (frameworkElement != null - && !(frameworkElement is FrameworkElement) - && !(frameworkElement is Image) - ) - { - // For IFrameworkElement implementers that are not FrameworkElements, the constraint logic must - // be performed by the parent. Otherwise, the native element will take the size it needs without - // regards to explicit XAML size characteristics. The Android ProgressBar is a good example of - // that behavior. - - var margin = frameworkElement.Margin; - - if (margin != Thickness.Empty) - { - // Apply the margin for framework elements, as if it were padding to the child. - slotSize = new Size( - Math.Max(0, slotSize.Width - margin.Left - margin.Right), - Math.Max(0, slotSize.Height - margin.Top - margin.Bottom) - ); - } - - // Alias the Dependency Properties values to avoid double calls. - var childWidth = frameworkElement.Width; - var childMaxWidth = frameworkElement.MaxWidth; - var childHeight = frameworkElement.Height; - var childMaxHeight = frameworkElement.MaxHeight; - - var optionalMaxWidth = !IsInfinity(childMaxWidth) && !IsNaN(childMaxWidth) ? childMaxWidth : (double?)null; - var optionalWidth = !IsNaN(childWidth) ? childWidth : (double?)null; - var optionalMaxHeight = !IsInfinity(childMaxHeight) && !IsNaN(childMaxHeight) ? childMaxHeight : (double?)null; - var optionalHeight = !IsNaN(childHeight) ? childHeight : (double?)null; - - // After the margin has been removed, ensure the remaining space slot does not go - // over the explicit or maximum size of the child. - if (optionalMaxWidth != null || optionalWidth != null) - { - var constrainedWidth = Math.Min( - optionalMaxWidth ?? double.PositiveInfinity, - optionalWidth ?? double.PositiveInfinity - ); - - slotSize.Width = Math.Min(slotSize.Width, constrainedWidth); - } - - if (optionalMaxHeight != null || optionalHeight != null) - { - var constrainedHeight = Math.Min( - optionalMaxHeight ?? double.PositiveInfinity, - optionalHeight ?? double.PositiveInfinity - ); - - slotSize.Height = Math.Min(slotSize.Height, constrainedHeight); - } - } - - ret = MeasureChildOverride(view, slotSize); - - if ( - IsPositiveInfinity(ret.Height) || IsPositiveInfinity(ret.Width) - ) - { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Error)) - { - var viewName = frameworkElement.SelectOrDefault(f => f.Name, "NativeView"); - var margin = frameworkElement.SelectOrDefault(f => f.Margin, Thickness.Empty); - - this.Log().ErrorFormat( - "[{0}/{1}] MeasureChild({2}/{3}/{4}/{5}) = Child returned INFINITY {6}", - LoggingOwnerTypeName, - Name, - view.GetType(), - viewName, - slotSize, - margin, - ret - ); - } - - ret = new Size(0, 0); - } - else - { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) - { - var viewName = frameworkElement.SelectOrDefault(f => f.Name, "NativeView"); - var margin = frameworkElement.SelectOrDefault(f => f.Margin, Thickness.Empty); - - this.Log().DebugFormat( - "[{0}/{1}] MeasureChild({2}/{3}/{4}/{5}) = {6}", - LoggingOwnerTypeName, - Name, - view.GetType(), - viewName, - slotSize, - margin, - ret - ); - } - } - - var hasLayouter = frameworkElement?.HasLayouter ?? false; - if (!hasLayouter || frameworkElement.Visibility == Visibility.Collapsed) - { - // For native controls only - because it's already set in Layouter.Measure() - // for Uno's managed controls - LayoutInformation.SetDesiredSize(view, ret); - } - - - return ret; - } - - /// - /// Arranges the location a child in the current panel - /// - /// The child instance - /// The rectangle to use, in Logical position - public void ArrangeChild(View view, Rect frame) - { - if ((view as IFrameworkElement)?.Visibility == Visibility.Collapsed) - { - return; - } - - LayoutInformation.SetLayoutSlot(view, frame); - - // Note: This is not matching Windows. - // Applying alignments should depend on what ArrangeOverride returns (as in Skia and Wasm). - var (finalFrame, clippedFrame) = ApplyMarginAndAlignments(view, frame); - if (view is UIElement elt) - { - elt.LayoutSlotWithMarginsAndAlignments = finalFrame; - elt.ClippedFrame = clippedFrame; - } - - - ArrangeChildOverride(view, finalFrame); - } - -#if __ANDROID__ || __IOS__ || __MACOS__ - private void LogArrange(View view, Rect frame) - { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) - { - var viewName = (view as IFrameworkElement).SelectOrDefault(f => f.Name, "NativeView"); - var margin = (view as IFrameworkElement).SelectOrDefault(f => f.Margin, Thickness.Empty); - - this.Log().DebugFormat("[{0}/{1}] ArrangeChild({2}/{3}/{4}/{5})", LoggingOwnerTypeName, Name, view.GetType(), viewName, frame, margin); - } - - } -#endif - - protected abstract string Name { get; } - - private (Rect layoutFrame, Rect clippedFrame) ApplyMarginAndAlignments(View view, Rect frame) - { - // In this implementation, since we do not have the ability to intercept properly the measure and arrange - // because of the type of hierarchy (inheriting from native views), we must apply the margins and alignments - // from within the panel to its children. This makes the authoring of custom panels that do not inherit from - // Panel that do not use this helper a bit more complex, but for all other panels that use this - // layouter, the logic is implied. - - // The result "layoutFrame" gives the positioning of the element, relative to the origin of the parent's frame. - // The result "clippedFrame" gives the resulting boundaries of the element. - // If clipping is required, that's were it should occurs. - - if (view is IFrameworkElement frameworkElement) - { - // Apply the margin for framework elements, as if it were padding to the child. - - var (x, y, width, height) = frame; - - // capture the child's state to avoid getting DependencyProperties values multiple times. - var childVerticalAlignment = frameworkElement.VerticalAlignment; - var childHorizontalAlignment = frameworkElement.HorizontalAlignment; - - AdjustAlignment(view, ref childHorizontalAlignment, ref childVerticalAlignment); - - var childMaxHeight = frameworkElement.MaxHeight; - var childMaxWidth = frameworkElement.MaxWidth; - var (childMinHeight, childMinWidth) = (frameworkElement as ILayoutOptOut)?.ShouldUseMinSize == false - ? (0, 0) - : (frameworkElement.MinHeight, frameworkElement.MinWidth); - var childWidth = frameworkElement.Width; - var childHeight = frameworkElement.Height; - var childMargin = frameworkElement.Margin; - - var hasChildHeight = !IsNaN(childHeight); - var hasChildWidth = !IsNaN(childWidth); - var hasChildMaxWidth = !IsInfinity(childMaxWidth) && !IsNaN(childMaxWidth); - var hasChildMaxHeight = !IsInfinity(childMaxHeight) && !IsNaN(childMaxHeight); - var hasChildMinWidth = childMinWidth > 0.0; - var hasChildMinHeight = childMinHeight > 0.0; - - if ( - childVerticalAlignment != VerticalAlignment.Stretch - || childHorizontalAlignment != HorizontalAlignment.Stretch - || hasChildWidth - || hasChildHeight - || hasChildMaxWidth - || hasChildMaxHeight - || hasChildMinWidth - || hasChildMinHeight - ) - { - var desiredSize = LayoutInformation.GetDesiredSize(view); - - // Apply vertical alignment - if ( - childVerticalAlignment != VerticalAlignment.Stretch - || hasChildHeight - || hasChildMaxHeight - || hasChildMinHeight - ) - { - var actualHeight = GetActualSize( - frame.Height, - childVerticalAlignment == VerticalAlignment.Stretch, - childMaxHeight, - childMinHeight, - childHeight, - childMargin.Top + childMargin.Bottom, - hasChildHeight, - hasChildMaxHeight, - hasChildMinHeight, - desiredSize.Height); - - if (actualHeight == frame.Height) - { - y = frame.Y; // nothing to align: we're using exactly the available height - - } - else - { - switch (childVerticalAlignment) - { - case VerticalAlignment.Top: - y = frame.Y; - break; - - case VerticalAlignment.Bottom: - y = frame.Y + frame.Height - actualHeight; - break; - - case VerticalAlignment.Stretch: - // On UWP, when a control is taking more height than available from - // parent, it will be top-aligned when its alignment is Stretch - y = frame.Y + Math.Max((frame.Height - actualHeight) / 2d, 0d); - break; - case VerticalAlignment.Center: - y = frame.Y + (frame.Height - actualHeight) / 2d; - break; - } - } - - height = actualHeight; - } - - // Apply horizontal alignment - if ( - childHorizontalAlignment != HorizontalAlignment.Stretch - || hasChildWidth - || hasChildMaxWidth - || hasChildMinWidth - ) - { - var actualWidth = GetActualSize( - frame.Width, - childHorizontalAlignment == HorizontalAlignment.Stretch, - childMaxWidth, - childMinWidth, - childWidth, - childMargin.Left + childMargin.Right, - hasChildWidth, - hasChildMaxWidth, - hasChildMinWidth, - desiredSize.Width); - - if (actualWidth == frame.Width) - { - x = frame.X; // nothing to align: we're using exactly the available width - } - else - { - switch (childHorizontalAlignment) - { - case HorizontalAlignment.Left: - x = frame.X; - break; - - case HorizontalAlignment.Right: - x = frame.X + frame.Width - actualWidth; - break; - - case HorizontalAlignment.Stretch: - // On UWP, when a control is taking more width than available from - // parent, it will be left-aligned when its alignment is Stretch - x = frame.X + Math.Max((frame.Width - actualWidth) / 2d, 0d); - break; - case HorizontalAlignment.Center: - x = frame.X + (frame.Width - actualWidth) / 2d; - break; - } - } - - width = actualWidth; - } - } - - // Calculate Create layoutFrame and apply child's margins - var layoutFrame = new Rect(x, y, width, height).DeflateBy(childMargin); - - // Give opportunity to element to alter arranged size - layoutFrame.Size = frameworkElement.AdjustArrange(layoutFrame.Size); - - // Calculate clipped frame. - var clippedFrameWithParentOrigin = - layoutFrame - .IntersectWith(frame.DeflateBy(childMargin)) - ?? Rect.Empty; - - // Rebase the origin of the clipped frame to layout - var clippedFrame = new Rect( - clippedFrameWithParentOrigin.X - layoutFrame.X, - clippedFrameWithParentOrigin.Y - layoutFrame.Y, - clippedFrameWithParentOrigin.Width, - clippedFrameWithParentOrigin.Height); - - return (layoutFrame, clippedFrame); - } - else - { - var layoutFrame = new Rect( - x: IsNaN(frame.X) ? 0 : frame.X, - y: IsNaN(frame.Y) ? 0 : frame.Y, - width: Math.Max(0, IsNaN(frame.Width) ? 0 : frame.Width), - height: Math.Max(0, IsNaN(frame.Height) ? 0 : frame.Height) - ); - - // Clipped frame & layout frame are the same for native elements - return (layoutFrame, layoutFrame); - } - } - - protected virtual void AdjustAlignment(View view, ref HorizontalAlignment childHorizontalAlignment, - ref VerticalAlignment childVerticalAlignment) - { - if (view is Image img && (img.Stretch == Stretch.None || img.Stretch == Stretch.Uniform)) - { - // Image is a special control and is using the Vertical/Horizontal Alignment - // to calculate the final position of the image. On UWP, there's no difference - // between "Stretch" and "Center": they all behave like "Center", so we need - // to do the same here. - if (childVerticalAlignment == VerticalAlignment.Stretch) - { - childVerticalAlignment = VerticalAlignment.Center; - } - - if (childHorizontalAlignment == HorizontalAlignment.Stretch) - { - childHorizontalAlignment = HorizontalAlignment.Center; - } - } - } - - private double GetActualSize( - double frameSize, - bool isStretch, - double childMaxSize, - double childMinSize, - double childSize, - double childMarginSize, - bool hasChildSize, - bool hasChildMaxSize, - bool hasChildMinSize, - double desiredSize) - { - var min = hasChildMinSize ? childMinSize + childMarginSize : NegativeInfinity; - var max = hasChildMaxSize ? childMaxSize + childMarginSize : PositiveInfinity; - if (!hasChildSize) - { - childSize = isStretch - ? frameSize - : desiredSize; // desired size always include margin, so no need to calculate it here - } - else - { - childSize += childMarginSize; - } - - return Math.Max( - Math.Min( - Math.Min(childSize, frameSize), - max), - min); - } - - /// - /// Measures the specified child. - /// - /// The view to measure - /// The maximum size the child can use. - /// The size the view requires. - /// - /// Provides the ability for external implementations to measure children. - /// Mainly used for compatibility with existing WPF/WinRT implementations. - /// - void ILayouter.ArrangeChild(View view, Rect frame) - { - ArrangeChild(view, frame); - } - - /// - /// Arranges the specified view. - /// - /// The view to arrange - /// The frame available for the child. - /// - /// Provides the ability for external implementations to measure children. - /// Mainly used for compatibility with existing WPF/WinRT implementations. - /// - Size ILayouter.MeasureChild(View view, Size slotSize) - { - return MeasureChild(view, slotSize); - } - - private string LoggingOwnerTypeName => ((object)Panel ?? this).GetType().Name; - - public override string ToString() => $"[{LoggingOwnerTypeName}.Layouter]" + (string.IsNullOrEmpty(Panel?.Name) ? default : $"(name='{Panel.Name}')"); - } -} -#endif diff --git a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.iOS.cs deleted file mode 100644 index fe455f4240b1..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.iOS.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Uno.Extensions; -using Uno; -using Uno.UI; -using Uno.Foundation.Logging; -using Uno.Collections; -using Microsoft.UI.Xaml.Media; -using Windows.Foundation; -using View = UIKit.UIView; -using UIKit; -using CoreGraphics; -using Uno.Disposables; -using ObjCRuntime; - -namespace Microsoft.UI.Xaml.Controls -{ - abstract partial class Layouter - { - public IEnumerable GetChildren() - { - return (Panel as UIView).GetChildren(); - } - - protected Size MeasureChildOverride(View view, Size slotSize) - { - var ret = view - .SizeThatFits(slotSize.LogicalToPhysicalPixels()) - .PhysicalToLogicalPixels() - .ToFoundationSize(); - - // With iOS, a child may return a size that fits that is larger than the suggested size. - // We don't want that with respects to the Xaml model, so we cap the size to the input constraints. - - if (!(view is FrameworkElement) && view is IFrameworkElement ife) - { - if (!(view is Image)) // Except for Image - { - // If the child is not a FrameworkElement, part of the "Measure" - // phase must be done by the parent element's layouter. - // Here, it means adding the margin to the measured size. - ret = ret.Add(ife.Margin); - } - } - - ret.Width = double.IsNaN(ret.Width) ? double.PositiveInfinity : Math.Min(slotSize.Width, ret.Width); - ret.Height = double.IsNaN(ret.Height) ? double.PositiveInfinity : Math.Min(slotSize.Height, ret.Height); - - return ret; - } - - protected void ArrangeChildOverride(View view, Rect frame) - { - var nativeFrame = ViewHelper.LogicalToPhysicalPixels(frame); - - if (nativeFrame != view.Frame) - { - LogArrange(view, nativeFrame); - - using (SettingFrame(view)) - { - view.Frame = nativeFrame; - } - } - } - - private void LogArrange(View view, CGRect frame) - { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) - { - LogArrange(view, (Rect)frame); - } - } - - /// - /// Handle the native in the (rare) case that this is a non-IFrameworkElement view with a - /// non-identity transform. - /// - private IDisposable SettingFrame(View view) - { - if (view is IFrameworkElement) - { - // This is handled directly in IFrameworkElement.Frame setter - return null; - } - - if (view.Transform.IsIdentity) - { - // Transform is identity anyway - return null; - } - - // If UIView.Transform is not identity, then modifying the frame will give undefined behavior. (https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/transform) - // We have either already applied the transform to the new frame, or we will reset the transform straight after. - var transform = view.Transform; - view.Transform = CGAffineTransform.MakeIdentity(); - return Disposable.Create(reapplyTransform); - - void reapplyTransform() - { - view.Transform = transform; - } - } - } -} diff --git a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.macOS.cs b/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.macOS.cs deleted file mode 100644 index 331f0dfb5898..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.macOS.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Uno.Extensions; -using Uno; -using Uno.UI; -using Uno.Foundation.Logging; -using Uno.Collections; -using Microsoft.UI.Xaml.Media; -using Windows.Foundation; - -using View = AppKit.NSView; -using AppKit; -using CoreGraphics; -using Uno.Disposables; -using CoreAnimation; -using ObjCRuntime; - -namespace Microsoft.UI.Xaml.Controls -{ - abstract partial class Layouter - { - public IEnumerable GetChildren() - { - return (Panel as NSView).GetChildren(); - } - - protected Size MeasureChildOverride(View view, Size slotSize) - { - var ret = view - .SizeThatFits(slotSize.LogicalToPhysicalPixels()) - .PhysicalToLogicalPixels() - .ToFoundationSize(); - - // With iOS, a child may return a size that fits that is larger than the suggested size. - // We don't want that with respects to the Xaml model, so we cap the size to the input constraints. - ret.Width = double.IsNaN(ret.Width) ? double.PositiveInfinity : Math.Min(slotSize.Width, ret.Width); - ret.Height = double.IsNaN(ret.Height) ? double.PositiveInfinity : Math.Min(slotSize.Height, ret.Height); - - return ret; - } - - protected void ArrangeChildOverride(View view, Rect frame) - { - var nativeFrame = ViewHelper.LogicalToPhysicalPixels(frame); - - if (nativeFrame != view.Frame) - { - LogArrange(view, nativeFrame); - - using (SettingFrame(view)) - { - view.Frame = nativeFrame; - - UpdateClip(view); - } - } - } - - private void LogArrange(View view, CGRect frame) - { - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) - { - LogArrange(view, (Rect)frame); - } - } - - private static void UpdateClip(View view) - { - // TODO - - //if (!FeatureConfiguration.UIElement.UseLegacyClipping) - //{ - // UIElement.UpdateMask(view, view.Superview); - - // foreach (var child in view.GetChildren()) - // { - // UIElement.UpdateMask(child, view); - // } - //} - } - - /// - /// Handle the native in the (rare) case that this is a non-IFrameworkElement view with a - /// non-identity transform. - /// - private IDisposable SettingFrame(View view) - { - if (view is IFrameworkElement) - { - // This is handled directly in IFrameworkElement.Frame setter - return null; - } - - var layer = view.Layer; - if (layer == null) - { - // Transform is identity anyway, or Layer is null - return null; - } - - // If NSView.Transform is not identity, then modifying the frame will give undefined behavior. (https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/NSView/transform) - // We have either already applied the transform to the new frame, or we will reset the transform straight after. - var transform = layer.Transform; - if (transform.IsIdentity) - { - return null; - } - transform = CATransform3D.Identity; - return Disposable.Create(reapplyTransform); - - void reapplyTransform() - { - layer.Transform = transform; - } - } - } -} diff --git a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.unittests.cs b/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.unittests.cs deleted file mode 100644 index 75d880e813b2..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.unittests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using Windows.Foundation; -using System.Linq; -using Uno.Disposables; -using System.Text; -using System.Threading.Tasks; -using Uno.Extensions; -using Uno; -using Uno.Foundation.Logging; -using View = Microsoft.UI.Xaml.UIElement; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Controls.Primitives; - -namespace Microsoft.UI.Xaml.Controls -{ - partial class Layouter : ILayouter - { - protected Size MeasureChildOverride(View view, Size slotSize) - { - view.Measure(slotSize); - - return view.DesiredSize; - } - - protected void ArrangeChildOverride(View view, Rect frame) - { - view.Arranged = frame; - view.LayoutSlotWithMarginsAndAlignments = frame; - - LayoutInformation.SetLayoutSlot(view, frame); - } - - protected Size DesiredChildSize(View view) - { - return view.DesiredSize; - } - } -} diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs index e8de2e5d73dd..e066cacf3683 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs @@ -279,13 +279,6 @@ public override UICollectionViewCell GetCell(UICollectionView collectionView, NS // Normally this happens when the SelectorItem.Content is set, but there's an edge case where after a refresh, a // container can be dequeued which happens to have had exactly the same DataContext as the new item. cell.ClearMeasuredSize(); - - // Ensure ClippedFrame from a previous recycled item doesn't persist which can happen in some cases, - // and cause it to be clipped when either axis was smaller. - if (cell.Content is { } contentControl) - { - contentControl.ClippedFrame = null; - } } Owner?.XamlParent?.TryLoadMoreItems(index); @@ -672,7 +665,8 @@ private CGSize GetTemplateSize(DataTemplate dataTemplate, NSString elementKind, Owner.XamlParent.AddSubview(BlockLayout); BlockLayout.AddSubview(container); // Measure with PositiveInfinity rather than MaxValue, since some views handle this better. - size = Owner.NativeLayout.Layouter.MeasureChild(container, availableSize); + container.Measure(availableSize); + size = container.DesiredSize; if ((size.Height > nfloat.MaxValue / 2 || size.Width > nfloat.MaxValue / 2) && this.Log().IsEnabled(LogLevel.Warning) @@ -764,7 +758,6 @@ public NativeListViewBase Owner private Orientation ScrollOrientation => Owner.NativeLayout.ScrollOrientation; private bool SupportsDynamicItemSizes => Owner.NativeLayout.SupportsDynamicItemSizes; - private ILayouter Layouter => Owner.NativeLayout.Layouter; internal string ElementKind { get; set; } @@ -952,7 +945,8 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin { using (InterceptSetNeedsLayout()) { - _measuredContentSize = Layouter.MeasureChild(Content, availableSize); + Content.Measure(availableSize); + _measuredContentSize = Content.DesiredSize; } } else @@ -962,7 +956,8 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin //Attach temporarily, because some Uno control (eg ItemsControl) are only measured correctly after MovedToWindow has been called InterceptSetNeedsLayout(); Owner.XamlParent.AddSubview(this); - _measuredContentSize = Layouter.MeasureChild(Content, availableSize); + Content.Measure(availableSize); + _measuredContentSize = Content.DesiredSize; } finally { @@ -1056,7 +1051,7 @@ public override void LayoutSubviews() if (Content != null) { - Layouter.ArrangeChild(Content, new Rect(0, 0, (float)size.Width, (float)size.Height)); + Content.Arrange(new Rect(0, 0, (float)size.Width, (float)size.Height)); // The item has to be arranged relative to this internal container (at 0,0), // but doing this the LayoutSlot[WithMargins] has been updated, diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs index f3ad99b066ad..0b6507ed495c 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs @@ -1019,7 +1019,7 @@ private Size TryMeasureChild(View child, Size slotSize, ViewType viewType) if (child.IsLayoutRequested || slotSize != previousAvailableSize) { - var size = _layouter.MeasureChild(child, slotSize); + var size = slotSize; // TODO _layouter.MeasureChild(child, slotSize); if (ShouldApplyChildStretch) { @@ -1094,7 +1094,7 @@ protected void LayoutChild(View child, GeneratorDirection direction, int extentO top = logicalBreadthOffset; } var frame = new global::Windows.Foundation.Rect(new global::Windows.Foundation.Point(left, top), size); - _layouter.ArrangeChild(child, frame); + //_layouter.ArrangeChild(child, frame); // Due to conversions between physical and logical coordinates, the actual child end can differ from the end we sent to the layouter by a little bit. Debug.Assert(direction == GeneratorDirection.Forward || Math.Abs(GetChildEndWithMargin(child) - extentOffset) < 2, GetAssertMessage("Extent offset not applied correctly")); diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.cs index 2539e39015c0..7b2658ce2fd3 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.cs @@ -34,8 +34,8 @@ protected enum RelativeHeaderPlacement { Inline, Adjacent } /// For layouting this is identical to but for it is the opposite of . public abstract Orientation ScrollOrientation { get; } #if !UNO_REFERENCE_API - private protected readonly ILayouter _layouter = new VirtualizingPanelLayouter(); - internal ILayouter Layouter => _layouter; + //private protected readonly ILayouter _layouter = new VirtualizingPanelLayouter(); + //internal ILayouter Layouter => _layouter; #endif #pragma warning disable 67 // Unused member @@ -282,32 +282,32 @@ public static (TSource Item, TComparable Value) MinWithSelector "VirtualizingPanelLayout"; - - protected override Size ArrangeOverride(Size finalSize) - { - throw new NotSupportedException($"{nameof(VirtualizingPanelLayouter)} is only used for measuring and arranging child views."); - } - -#if __ANDROID__ - protected override void MeasureChild(Android.Views.View view, int widthSpec, int heightSpec) - { - view.Measure(widthSpec, heightSpec); - } -#endif - - protected override Size MeasureOverride(Size availableSize) - { - throw new NotSupportedException($"{nameof(VirtualizingPanelLayouter)} is only used for measuring and arranging child views."); - } - } + // private class VirtualizingPanelLayouter : Layouter + // { + + // public VirtualizingPanelLayouter() : base(null) + // { + + // } + // protected override string Name => "VirtualizingPanelLayout"; + + // protected override Size ArrangeOverride(Size finalSize) + // { + // throw new NotSupportedException($"{nameof(VirtualizingPanelLayouter)} is only used for measuring and arranging child views."); + // } + + //#if __ANDROID__ + // protected override void MeasureChild(Android.Views.View view, int widthSpec, int heightSpec) + // { + // view.Measure(widthSpec, heightSpec); + // } + //#endif + + // protected override Size MeasureOverride(Size availableSize) + // { + // throw new NotSupportedException($"{nameof(VirtualizingPanelLayouter)} is only used for measuring and arranging child views."); + // } + // } #endif } } diff --git a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.Android.cs index c96d78b7aace..ee68877cfda8 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ScrollContentPresenter/NativeScrollContentPresenter.Android.cs @@ -46,7 +46,7 @@ public ScrollBarVisibility HorizontalScrollBarVisibility } } - private ILayouter _layouter; + //private ILayouter _layouter; private readonly WeakReference _scrollViewer; public NativeScrollContentPresenter(ScrollViewer scroller) : this() @@ -65,7 +65,7 @@ public NativeScrollContentPresenter() SetClipChildren(false); ScrollBarStyle = ScrollbarStyles.OutsideOverlay; // prevents padding from affecting scrollbar position - _layouter = new ScrollViewerLayouter(this); + //_layouter = new ScrollViewerLayouter(this); } private void InitializeScrollbars() @@ -103,21 +103,89 @@ partial void OnContentChanged(View previousView, View newView) } } - ILayouter ILayouterElement.Layouter => _layouter; - Size ILayouterElement.LastAvailableSize => LayoutInformation.GetAvailableSize(this); - bool ILayouterElement.IsMeasureDirty => true; - bool ILayouterElement.IsFirstMeasureDoneAndManagedElement => false; - bool ILayouterElement.StretchAffectsMeasure => false; - bool ILayouterElement.IsMeasureDirtyPathDisabled => true; - protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { - ((ILayouterElement)this).OnMeasureInternal(widthMeasureSpec, heightMeasureSpec); + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); } - void ILayouterElement.SetMeasuredDimensionInternal(int width, int height) + Size ILayouterElement.Measure(Size availableSize) { - SetMeasuredDimension(width, height); + var child = this.GetChildren().FirstOrDefault(); + + var desiredChildSize = default(Size); + if (child != null) + { + var scrollSpace = availableSize; + if (VerticalScrollBarVisibility != ScrollBarVisibility.Disabled) + { + scrollSpace.Height = double.PositiveInfinity; + } + if (HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled) + { + scrollSpace.Width = double.PositiveInfinity; + } + + if (child is UIElement childAsUIElement) + { + var childMargin = (child as FrameworkElement)?.Margin ?? Thickness.Empty; + SetChildMargin(childMargin); + + childAsUIElement.Measure(scrollSpace); + desiredChildSize = childAsUIElement.DesiredSize; + } + else if (child is ILayouterElement layouterElement) + { + desiredChildSize = layouterElement.Measure(scrollSpace); + } + else + { + // TODO: + } + + // Give opportunity to the the content to define the viewport size itself + (child as ICustomScrollInfo)?.ApplyViewport(ref desiredChildSize); + } + + return desiredChildSize; + } + + void ILayouterElement.Arrange(Rect finalRect) + { + var child = this.GetChildren().FirstOrDefault(); + if (child != null) + { + var desiredChildSize = LayoutInformation.GetDesiredSize(child); + + var occludedPadding = _padding; + var slotSize = finalRect.Size; + slotSize.Width -= occludedPadding.Left + occludedPadding.Right; + slotSize.Height -= occludedPadding.Top + occludedPadding.Bottom; + + var width = Math.Max(slotSize.Width, desiredChildSize.Width); + var height = Math.Max(slotSize.Height, desiredChildSize.Height); + + if (child is UIElement childAsUIElement) + { + childAsUIElement.Arrange(new Rect(0, 0, width, height)); + } + else if (child is ILayouterElement layouterElement) + { + layouterElement.Arrange(new Rect(0, 0, width, height)); + } + else + { + // TODO: + } + + ScrollOwner?.TryApplyPendingScrollTo(); + + // Give opportunity to the the content to define the viewport size itself + (child as ICustomScrollInfo)?.ApplyViewport(ref slotSize); + + var logicalRect = new Rect(0, 0, slotSize.Width, slotSize.Height); + var physical = logicalRect.LogicalToPhysicalPixels(); + this.Layout((int)physical.Left, (int)physical.Top, (int)physical.Right, (int)physical.Bottom); + } } partial void OnLayoutPartial(bool changed, int left, int top, int right, int bottom) @@ -129,7 +197,7 @@ partial void OnLayoutPartial(bool changed, int left, int top, int right, int bot // may leave the default ScrollViewer implementation place // the child at an invalid location when the visibility changes. - _layouter.Arrange(newSize); + //_layouter.Arrange(newSize); // base.OnLayout is not invoked in the mixin to allow for the clipping algorithms base.OnLayout(changed, left, top, right, bottom); @@ -149,83 +217,6 @@ private void UpdateScrollSettings() IsScrollingEnabled = verticalScrollEnabled || horizontalScrollEnabled; } - private class ScrollViewerLayouter : Layouter - { - public ScrollViewerLayouter(NativeScrollContentPresenter view) : base(view) - { - } - - private NativeScrollContentPresenter ScrollContentPresenter => Panel as NativeScrollContentPresenter; - - protected override void MeasureChild(View child, int widthSpec, int heightSpec) - { - var childMargin = (child as FrameworkElement)?.Margin ?? Thickness.Empty; - ScrollContentPresenter.SetChildMargin(childMargin); - - this.GetChildren().FirstOrDefault()?.Measure(widthSpec, heightSpec); - } - - protected override Size MeasureOverride(Size availableSize) - { - var child = this.GetChildren().FirstOrDefault(); - - var desiredChildSize = default(Size); - if (child != null) - { - var scrollSpace = availableSize; - if (ScrollContentPresenter.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled) - { - scrollSpace.Height = double.PositiveInfinity; - } - if (ScrollContentPresenter.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled) - { - scrollSpace.Width = double.PositiveInfinity; - } - - desiredChildSize = MeasureChild(child, scrollSpace); - - // Give opportunity to the the content to define the viewport size itself - (child as ICustomScrollInfo)?.ApplyViewport(ref desiredChildSize); - } - - return desiredChildSize; - } - - protected override Size ArrangeOverride(Size slotSize) - { - var child = this.GetChildren().FirstOrDefault(); - - if (child != null) - { - var desiredChildSize = LayoutInformation.GetDesiredSize(child); - - var occludedPadding = ScrollContentPresenter._padding; - slotSize.Width -= occludedPadding.Left + occludedPadding.Right; - slotSize.Height -= occludedPadding.Top + occludedPadding.Bottom; - - var width = Math.Max(slotSize.Width, desiredChildSize.Width); - var height = Math.Max(slotSize.Height, desiredChildSize.Height); - - ArrangeChild(child, new Rect( - 0, - 0, - width, - height - )); - - ScrollContentPresenter.ScrollOwner?.TryApplyPendingScrollTo(); - - // Give opportunity to the the content to define the viewport size itself - (child as ICustomScrollInfo)?.ApplyViewport(ref slotSize); - - } - - return slotSize; - } - - protected override string Name => Panel.Name; - } - #region Managed to native private Thickness _padding; Thickness INativeScrollContentPresenter.Padding diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs index db97c127484a..21dbfc4ff3a9 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs @@ -287,13 +287,13 @@ private Java.Lang.ICharSequence GetTextFormatted() #region Layout - internal protected override void OnInvalidateMeasure() - { - base.OnInvalidateMeasure(); + //internal protected override void OnInvalidateMeasure() + //{ + // base.OnInvalidateMeasure(); - // We want to invalidate both the layout and the rendering - Invalidate(); - } + // // We want to invalidate both the layout and the rendering + // Invalidate(); + //} protected override Size MeasureOverride(Size availableSize) { diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs index abe6cd225217..656c688681c8 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOS.cs @@ -76,15 +76,15 @@ public override void Draw(CGRect rect) } } - /// - /// Invalidates the last cached measure - /// - protected internal override void OnInvalidateMeasure() - { - base.OnInvalidateMeasure(); - SetNeedsDisplay(); - _measureInvalidated = true; - } + ///// + ///// Invalidates the last cached measure + ///// + //protected internal override void OnInvalidateMeasure() + //{ + // base.OnInvalidateMeasure(); + // SetNeedsDisplay(); + // _measureInvalidated = true; + //} #region Layout diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.Android.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.Android.cs index 97c1bd3d056e..dfe4d7577a4e 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.Android.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.Android.cs @@ -236,12 +236,10 @@ internal override bool GetDefaultValue2(DependencyProperty property, out object protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { - ((ILayouterElement)this).OnMeasureInternal(widthMeasureSpec, heightMeasureSpec); - } + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); - void ILayouterElement.SetMeasuredDimensionInternal(int width, int height) - { - SetMeasuredDimension(width, height); + global::System.Diagnostics.Debug.WriteLine($"Measured {this}: {ViewHelper.PhysicalToLogicalPixels(MeasuredWidth)}x{ViewHelper.PhysicalToLogicalPixels(MeasuredHeight)}"); + //((ILayouterElement)this).OnMeasureInternal(widthMeasureSpec, heightMeasureSpec); } protected override void OnLayoutCore(bool changed, int left, int top, int right, int bottom, bool localIsLayoutRequested) @@ -249,57 +247,6 @@ protected override void OnLayoutCore(bool changed, int left, int top, int right, try { base.OnLayoutCore(changed, left, top, right, bottom, localIsLayoutRequested); - - Rect finalRect; - if (TransientArrangeFinalRect is Rect tafr) - { - // If the parent element is from managed code, - // we can recover the "Arrange" with double accuracy. - // We use that because the conversion to android's "int" is loosing too much precision. - finalRect = tafr; - } - else - { - // Here the "arrange" is coming from a native element, - // so we convert those measurements to logical ones. - finalRect = new Rect(left, top, right - left, bottom - top).PhysicalToLogicalPixels(); - - // We also need to set the LayoutSlot as it was not set by the parent. - // Note: This is only an approximation of the LayoutSlot as margin and alignment might already been applied at this point. - LayoutInformation.SetLayoutSlot(this, finalRect); - LayoutSlotWithMarginsAndAlignments = finalRect; - } - - if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug)) - { - this.Log().DebugFormat( - "[{0}/{1}] OnLayoutCore({2}, {3}, {4}, {5}) (parent: {5},{6})", - GetType(), - Name, - left, top, right, bottom, - MeasuredWidth, - MeasuredHeight - ); - } - - if ( - // If the layout has changed, but the final size has not, this is just a translation. - // So unless there was a layout requested, we can skip arranging the children. - (changed && _lastLayoutSize != finalRect.Size) - - // Even if nothing changed, but a layout was requested, arrange the children. - // Use the copy grabbed from the native invocation to avoid an additional interop call - || localIsLayoutRequested - ) - { - _lastLayoutSize = finalRect.Size; - - OnBeforeArrange(); - - _layouter.Arrange(finalRect); - - OnAfterArrange(); - } } catch (Exception e) { diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs index 78684e5c86b8..6ec188ab6969 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs @@ -22,11 +22,8 @@ namespace Microsoft.UI.Xaml { public partial class FrameworkElement { - private readonly static IEventProvider _trace = Tracing.Get(FrameworkElement.TraceProvider.Id); - private bool m_firedLoadingEvent; - private const double SIZE_EPSILON = 0.05d; private readonly Size MaxSize = new Size(double.PositiveInfinity, double.PositiveInfinity); private protected string DepthIndentation @@ -127,254 +124,6 @@ void InvokeLoadedWithTry() partial void OnLoadedPartial(); - internal sealed override void MeasureCore(Size availableSize) - { - if (_trace.IsEnabled) - { - /// - /// This method contains or is called by a try/catch containing method and - /// can be significantly slower than other methods as a result on WebAssembly. - /// See https://github.com/dotnet/runtime/issues/56309 - /// - void MeasureCoreWithTrace(Size availableSize) - { - var traceActivity = _trace.WriteEventActivity( - TraceProvider.FrameworkElement_MeasureStart, - TraceProvider.FrameworkElement_MeasureStop, - new object[] { GetType().Name, this.GetDependencyObjectId(), Name, availableSize.ToString() } - ); - - using (traceActivity) - { - InnerMeasureCore(availableSize); - } - } - - MeasureCoreWithTrace(availableSize); - } - else - { - // This method is split in two functions to avoid the dynCalls - // invocations generation for mono-wasm AOT inside of try/catch/finally blocks. - InnerMeasureCore(availableSize); - } - - } - - private void InnerMeasureCore(Size availableSize) - { - if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) - { - this.Log().LogWarning($"[LayoutCycleTracing] Measuring {this},{this.GetDebugName()} with availableSize {availableSize}."); - } - - // Uno TODO - //CLayoutManager* pLayoutManager = VisualTree::GetLayoutManagerForElement(this); - //bool bInLayoutTransition = pLayoutManager ? pLayoutManager->GetTransitioningElement() == this : false; - - Size frameworkAvailableSize = default; - double minWidth = 0.0f; - double maxWidth = 0.0f; - double minHeight = 0.0f; - double maxHeight = 0.0f; - - double clippedDesiredWidth; - double clippedDesiredHeight; - - double marginWidth = 0.0f; - double marginHeight = 0.0f; - - //bool bTemplateApplied = false; - - RaiseLoadingEventIfNeeded(); - - //if (!bInLayoutTransition) - { - // Templates should be applied here. - //bTemplateApplied = InvokeApplyTemplate(); - - // TODO: BEGIN Uno specific - if (this is Control thisAsControl) - { - thisAsControl.ApplyTemplate(); - - // Update bindings to ensure resources defined - // in visual parents get applied. - this.UpdateResourceBindings(); - } - // TODO: END Uno specific - - // Subtract the margins from the available size - var margin = Margin; - marginWidth = margin.Left + margin.Right; - marginHeight = margin.Top + margin.Bottom; - - // We check to see if availableSize.width and availableSize.height are finite since that will - // also protect against NaN getting in. - frameworkAvailableSize.Width = double.IsFinite(availableSize.Width) ? Math.Max(availableSize.Width - marginWidth, 0) : double.PositiveInfinity; - frameworkAvailableSize.Height = double.IsFinite(availableSize.Height) ? Math.Max(availableSize.Height - marginHeight, 0) : double.PositiveInfinity; - - // Layout transforms would get processed here. - - // Adjust available size by Min/Max Width/Height - - var (minSize, maxSize) = this.GetMinMax(); - minWidth = minSize.Width; - minHeight = minSize.Height; - maxWidth = maxSize.Width; - maxHeight = maxSize.Height; - - frameworkAvailableSize.Width = Math.Max(minWidth, Math.Min(frameworkAvailableSize.Width, maxWidth)); - frameworkAvailableSize.Height = Math.Max(minHeight, Math.Min(frameworkAvailableSize.Height, maxHeight)); - } - //else - //{ - // // when in a transition, just take the passed in constraint without considering the above - // frameworkAvailableSize = availableSize; - //} - - var desiredSize = MeasureOverride(frameworkAvailableSize); - - // We need to round now since we save the values off, and use them to determine - // if a layout clip will be applied. - if (GetUseLayoutRounding()) - { - desiredSize.Width = LayoutRound(desiredSize.Width); - desiredSize.Height = LayoutRound(desiredSize.Height); - - } - - //if (!bInLayoutTransition) - { - // Maximize desired size with user provided min size. It's also possible that MeasureOverride returned NaN for either - // width or height, in which case we should use the min size as well. - desiredSize.Width = Math.Max(desiredSize.Width, minWidth); - if (double.IsNaN(desiredSize.Width)) - { - desiredSize.Width = minWidth; - } - desiredSize.Height = Math.Max(desiredSize.Height, minHeight); - if (double.IsNaN(desiredSize.Height)) - { - desiredSize.Height = minHeight; - } - - // We need to round now since we save the values off, and use them to determine - // if a layout clip will be applied. - - if (GetUseLayoutRounding()) - { - desiredSize.Width = LayoutRound(desiredSize.Width); - desiredSize.Height = LayoutRound(desiredSize.Height); - } - - // Here is the "true minimum" desired size - the one that is - // for sure enough for the control to render its content. - EnsureLayoutStorage(); - m_unclippedDesiredSize = desiredSize; - - // More layout transforms processing here. - - if (desiredSize.Width > maxWidth) - { - desiredSize.Width = maxWidth; - } - - if (desiredSize.Height > maxHeight) - { - desiredSize.Height = maxHeight; - } - - // Transform desired size to layout slot space (placeholder for when we do layout transforms) - - // Layout round the margins too. This corresponds to the behavior in ArrangeCore, where we check the unclipped desired - // size against available space minus the rounded margin. This also prevents a bug where MeasureCore adds the unrounded - // margin (e.g. 14) to the desired size (e.g. 55.56) and rounds the final result (69.56 rounded to 69.33 under 2.25x scale), - // then ArrangeCore takes that rounded result (i.e. 69.33), subtracts the unrounded margin (i.e. 14) and ends up with a - // size smaller than the desired size (69.33 - 14 = 55.33 < 55.56). This ends up putting a layout clip on an element that - // doesn't need one, and causes big problems if the element is the scrollable extent of a carousel panel. - double roundedMarginWidth = marginWidth; - double roundedMarginHeight = marginHeight; - if (GetUseLayoutRounding()) - { - roundedMarginWidth = LayoutRound(marginWidth); - roundedMarginHeight = LayoutRound(marginHeight); - } - - // Because of negative margins, clipped desired size may be negative. - // Need to keep it as XFLOATS for that reason and maximize with 0 at the - // very last point - before returning desired size to the parent. - clippedDesiredWidth = desiredSize.Width + roundedMarginWidth; - clippedDesiredHeight = desiredSize.Height + roundedMarginHeight; - - // only clip and constrain if the tree wants that. - // currently only listviewitems do not want clipping - // UNO TODO - - //if (!pLayoutManager->GetIsInNonClippingTree()) - { - // In overconstrained scenario, parent wins and measured size of the child, - // including any sizes set or computed, can not be larger then - // available size. We will clip the guy later. - if (clippedDesiredWidth > availableSize.Width) - { - clippedDesiredWidth = availableSize.Width; - } - - if (clippedDesiredHeight > availableSize.Height) - { - clippedDesiredHeight = availableSize.Height; - } - } - - // Note: unclippedDesiredSize is needed in ArrangeCore, - // because due to the layout protocol, arrange should be called - // with constraints greater or equal to child's desired size - // returned from MeasureOverride. But in most circumstances - // it is possible to reconstruct original unclipped desired size. - - desiredSize.Width = Math.Max(0, clippedDesiredWidth); - desiredSize.Height = Math.Max(0, clippedDesiredHeight); - } - //else - //{ - // // in LT, need to take precautions - // desiredSize.Width = Math.Max(desiredSize.Width, 0.0f); - // desiredSize.Height = Math.Max(desiredSize.Height, 0.0f); - //} - - // We need to round again in case the desired size has been modified since we originally - // rounded it. - if (GetUseLayoutRounding()) - { - desiredSize.Width = LayoutRound(desiredSize.Width); - desiredSize.Height = LayoutRound(desiredSize.Height); - } - - if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) - { - this.Log().LogWarning($"[LayoutCycleTracing] Measured {this},{this.GetDebugName()}: desiredSize is {desiredSize}."); - } - -#if __SKIA__ - if (desiredSize != DesiredSize) -#endif - { - // DesiredSize must include margins - m_desiredSize = desiredSize; -#if __SKIA__ - this.OnDesiredSizeChanged(); -#endif - } - - _logDebug?.Debug($"{DepthIndentation}[{FormatDebugName()}] Measure({Name}/{availableSize}/{Margin}) = {desiredSize} _unclippedDesiredSize={m_unclippedDesiredSize}"); - } - - private protected virtual void OnDesiredSizeChanged() - { - - } - private void RaiseLoadingEventIfNeeded() { if (!m_firedLoadingEvent //&& @@ -400,519 +149,6 @@ private void RaiseLoadingEventIfNeeded() } } - private string FormatDebugName() - => $"[{this}/{Name}"; - - internal sealed override void ArrangeCore(Rect finalRect) - { - if (_trace.IsEnabled) - { - void ArrangeCoreWithTrace(Rect finalRect) - { - var traceActivity = _trace.WriteEventActivity( - TraceProvider.FrameworkElement_ArrangeStart, - TraceProvider.FrameworkElement_ArrangeStop, - new object[] { GetType().Name, this.GetDependencyObjectId(), Name, finalRect.ToString() } - ); - - using (traceActivity) - { - InnerArrangeCore(finalRect); - } - } - - ArrangeCoreWithTrace(finalRect); - } - else - { - // This method is split in two functions to avoid the dynCalls - // invocations generation for mono-wasm AOT inside of try/catch/finally blocks. - InnerArrangeCore(finalRect); - } - - } - - private static bool IsLessThanAndNotCloseTo(double a, double b) => a < (b - SIZE_EPSILON); - - private void InnerArrangeCore(Rect finalRect) - { - _logDebug?.Debug($"{DepthIndentation}{FormatDebugName()}: InnerArrangeCore({finalRect})"); - if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) - { - this.Log().LogWarning($"[LayoutCycleTracing] Arranging {this},{this.GetDebugName()} with finalRect {finalRect}."); - } - - // Uno TODO: - //CLayoutManager* pLayoutManager = VisualTree::GetLayoutManagerForElement(this); - //bool bInLayoutTransition = pLayoutManager ? pLayoutManager->GetTransitioningElement() == this : false; - - bool needsClipBounds = false; - - var arrangeSize = finalRect.Size; - - var margin = Margin; - var marginWidth = margin.Left + margin.Right; - var marginHeight = margin.Top + margin.Bottom; - - var ha = HorizontalAlignment; - var va = VerticalAlignment; - - Size unclippedDesiredSize = default; - double minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0; - double effectiveMaxWidth = 0, effectiveMaxHeight = 0; - - Size oldRenderSize = default; - Size innerInkSize = default; - Size clippedInkSize = default; - Size clientSize = default; - double offsetX = 0, offsetY = 0; - - EnsureLayoutStorage(); - - unclippedDesiredSize = m_unclippedDesiredSize; - oldRenderSize = RenderSize; - - //if (!bInLayoutTransition) - { - Size arrangeSizeWithoutMargin = new Size( - Math.Max(arrangeSize.Width - marginWidth, 0), - Math.Max(arrangeSize.Height - marginHeight, 0) - ); - - var roundedMarginWidth = marginWidth; - var roundedMarginHeight = marginHeight; - - if (GetUseLayoutRounding()) - { - roundedMarginWidth = LayoutRound(marginWidth); - roundedMarginHeight = LayoutRound(marginHeight); - } - - // We handle layout rounding inconsistently across our code, this change is restricted to using layout - // rounding for margin only on certain scenarios to avoid introducing unexpected problems. - // If further rounding issues appear we should consider opening this behaviour to more scenarios. - if (roundedMarginWidth != marginWidth && arrangeSizeWithoutMargin.Width != unclippedDesiredSize.Width) - { - double arrangeWidthWithoutRoundedMargin = Math.Max(arrangeSize.Width - roundedMarginWidth, 0); - if (arrangeWidthWithoutRoundedMargin == unclippedDesiredSize.Width) - { - // The rounding difference between arrangeSizeWithoutMargin.width and unclippedDesiredSize.width - // comes from the horizontal margin. The rounded value of that margin must be used so that this - // FrameworkElement's ActualWidth does not return an incorrect value. - marginWidth = roundedMarginWidth; - arrangeSize.Width = arrangeWidthWithoutRoundedMargin; - } - else - { - arrangeSize.Width = arrangeSizeWithoutMargin.Width; - } - } - else - { - arrangeSize.Width = arrangeSizeWithoutMargin.Width; - } - - if (roundedMarginHeight != marginHeight && arrangeSizeWithoutMargin.Height != unclippedDesiredSize.Height) - { - double arrangeHeightWithoutRoundedMargin = Math.Max(arrangeSize.Height - roundedMarginHeight, 0); - if (arrangeHeightWithoutRoundedMargin == unclippedDesiredSize.Height) - { - // The rounding difference between arrangeSizeWithoutMargin.height and unclippedDesiredSize.height - // comes from the vertical margin. The rounded value of that margin must be used so that this - // FrameworkElement's ActualHeight does not return an incorrect value. - marginHeight = roundedMarginHeight; - arrangeSize.Height = arrangeHeightWithoutRoundedMargin; - } - else - { - arrangeSize.Height = arrangeSizeWithoutMargin.Height; - } - } - else - { - arrangeSize.Height = arrangeSizeWithoutMargin.Height; - } - - if (IsLessThanAndNotCloseTo(arrangeSize.Width, unclippedDesiredSize.Width)) - { - needsClipBounds = true; - arrangeSize.Width = unclippedDesiredSize.Width; - } - - if (IsLessThanAndNotCloseTo(arrangeSize.Height, unclippedDesiredSize.Height)) - { - needsClipBounds = true; - arrangeSize.Height = unclippedDesiredSize.Height; - } - - // Alignment==Stretch --> arrange at the slot size minus margins - // Alignment!=Stretch --> arrange at the unclippedDesiredSize - if (ha != HorizontalAlignment.Stretch) - { - arrangeSize.Width = unclippedDesiredSize.Width; - } - - if (va != VerticalAlignment.Stretch) - { - arrangeSize.Height = unclippedDesiredSize.Height; - } - - var (minSize, maxSize) = this.GetMinMax(); - minWidth = minSize.Width; - maxWidth = maxSize.Width; - minHeight = minSize.Height; - maxHeight = maxSize.Height; - - // Layout transforms processed here - - // We have to choose max between UnclippedDesiredSize and Max here, because - // otherwise setting of max property could cause arrange at less then unclippedDS. - // Clipping by Max is needed to limit stretch here - - effectiveMaxWidth = Math.Max(unclippedDesiredSize.Width, maxWidth); - if (IsLessThanAndNotCloseTo(effectiveMaxWidth, arrangeSize.Width)) - { - needsClipBounds = true; - arrangeSize.Width = effectiveMaxWidth; - } - - effectiveMaxHeight = Math.Max(unclippedDesiredSize.Height, maxHeight); - if (IsLessThanAndNotCloseTo(effectiveMaxHeight, arrangeSize.Height)) - { - needsClipBounds = true; - arrangeSize.Height = effectiveMaxHeight; - } - } - - innerInkSize = ArrangeOverride(arrangeSize); - - // Here we use un-clipped InkSize because element does not know that it is - // clipped by layout system and it shoudl have as much space to render as - // it returned from its own ArrangeOverride - // Inner ink size is not guaranteed to be rounded, but should be. - // TODO: inner ink size currently only rounded if plateau > 1 to minimize impact in RC, - // but should be consistently rounded in all plateaus. - var scale = RootScale.GetRasterizationScaleForElement(this); - if ((scale != 1.0f) && GetUseLayoutRounding()) - { - innerInkSize.Width = LayoutRound(innerInkSize.Width); - innerInkSize.Height = LayoutRound(innerInkSize.Height); - } - RenderSize = innerInkSize; - - //if (!IsSameSize(oldRenderSize, innerInkSize)) - //{ - // OnActualSizeChanged(); - //} - - if (oldRenderSize != innerInkSize) - { - this.GetContext().EventManager.EnqueueForSizeChanged(this, oldRenderSize); - } - - //if (!bInLayoutTransition) - { - // ClippedInkSize differs from InkSize only what MaxWidth/Height explicitly clip the - // otherwise good arrangement. For ex, DSMaxWidth - in this - // case we should initiate clip at MaxWidth and only show Top-Left portion - // of the element limited by Max properties. It is Top-left because in case when we - // are clipped by container we also degrade to Top-Left, so we are consistent. - clippedInkSize.Width = Math.Min(innerInkSize.Width, maxWidth); - clippedInkSize.Height = Math.Min(innerInkSize.Height, maxHeight); - - // remember we have to clip if Max properties limit the inkSize - needsClipBounds |= - IsLessThanAndNotCloseTo(clippedInkSize.Width, innerInkSize.Width) - || IsLessThanAndNotCloseTo(clippedInkSize.Height, innerInkSize.Height); - - // Transform stuff here - - // Note that inkSize now can be bigger then layoutSlotSize-margin (because of layout - // squeeze by the parent or LayoutConstrained=true, which clips desired size in Measure). - - // The client size is the size of layout slot decreased by margins. - // This is the "window" through which we see the content of the child. - // Alignments position ink of the child in this "window". - // Max with 0 is necessary because layout slot may be smaller then unclipped desired size. - clientSize.Width = Math.Max(0, finalRect.Width - marginWidth); - clientSize.Height = Math.Max(0, finalRect.Height - marginHeight); - - // Remember we have to clip if clientSize limits the inkSize - needsClipBounds |= - IsLessThanAndNotCloseTo(clientSize.Width, clippedInkSize.Width) - || IsLessThanAndNotCloseTo(clientSize.Height, clippedInkSize.Height); - - //bool isAlignedByDirectManipulation = IsAlignedByDirectManipulation(); - - //if (isAlignedByDirectManipulation) - //{ - // // Skip the layout engine's contribution to the element's offsets when it is already aligned by DirectManipulation. - - // if (m_pLayoutProperties.m_horizontalAlignment == HorizontalAlignment.Stretch) - // { - // // Check if the Stretch alignment needs to be overridden with a Left alignment. - // // The "IsStretchHorizontalAlignmentTreatedAsLeft" case corresponds to CFrameworkElement::ComputeAlignmentOffset's "degenerate Stretch to Top-Left" branch. - // // The "IsFinalArrangeSizeMaximized()" case is for text controls CTextBlock, CRichTextBlock and CRichTextBlockOverflow which stretch their desired width to the finalSize argument in their ArrangeOverride method. - // // The "(clippedInkSize.width == clientSize.width && unclippedDesiredSize.width < clientSize.width)" case is for 3rd party controls that stretch their desired width to the final arrange width too. - // bool isStretchAlignmentTreatedAsNear_New = - // IsStretchHorizontalAlignmentTreatedAsLeft(HorizontalAlignment.Stretch, clientSize, clippedInkSize) || - // (clippedInkSize.Width == clientSize.Width && unclippedDesiredSize.Width < clientSize.Width) || - // IsFinalArrangeSizeMaximized(); - - // // Check if the overriding needs are changing by accessing the current status from the owning ScrollViewer control. - // bool isStretchAlignmentTreatedAsNear_Old = IsStretchAlignmentTreatedAsNear(true /*isForHorizontalAlignment*/); - // if (isStretchAlignmentTreatedAsNear_New != isStretchAlignmentTreatedAsNear_Old) - // { - // // The overriding needs are changing - push the new status to the owning ScrollViewer control. - // OnAlignmentChanged(true /*fIsForHorizontalAlignment*/, true/*fIsForStretchAlignment*/, isStretchAlignmentTreatedAsNear_New); - // } - // } - - // if (m_pLayoutProperties.m_verticalAlignment == VerticalAlignment.Stretch) - // { - // // Check if the Stretch alignment needs to be overridden with a Top alignment. - // // The "IsStretchVerticalAlignmentTreatedAsTop" case corresponds to CFrameworkElement::ComputeAlignmentOffset's "degenerate Stretch to Top-Left" branch. - // // The "IsFinalArrangeSizeMaximized()" case is for text controls CTextBlock, CRichTextBlock and CRichTextBlockOverflow which stretch their desired height to the finalSize argument in their ArrangeOverride method. - // // The "(clippedInkSize.height == clientSize.height && unclippedDesiredSize.height < clientSize.height)" case is for 3rd party controls that stretch their desired height to the final arrange height too. - // bool isStretchAlignmentTreatedAsNear_New = - // IsStretchVerticalAlignmentTreatedAsTop(VerticalAlignment.Stretch, clientSize, clippedInkSize) || - // (clippedInkSize.Height == clientSize.Height && unclippedDesiredSize.Height < clientSize.Height) || - // IsFinalArrangeSizeMaximized(); - - // // Check if the overriding needs are changing by accessing the current status from the owning ScrollViewer control. - // bool isStretchAlignmentTreatedAsNear_Old = IsStretchAlignmentTreatedAsNear(false /*isForHorizontalAlignment*/); - // if (isStretchAlignmentTreatedAsNear_New != isStretchAlignmentTreatedAsNear_Old) - // { - // // The overriding needs are changing - push the new status to the owning ScrollViewer control. - // OnAlignmentChanged(false /*fIsForHorizontalAlignment*/, true /*fIsForStretchAlignment*/, isStretchAlignmentTreatedAsNear_New); - // } - // } - //} - //else - { - var offset = this.GetAlignmentOffset(clientSize, clippedInkSize); - offsetX = offset.X; - offsetY = offset.Y; - } - - //oldOffset = VisualOffset; - - //VisualOffset.x = offsetX + finalRect.X + m_pLayoutProperties->m_margin.left; - //VisualOffset.y = offsetY + finalRect.Y + m_pLayoutProperties->m_margin.top; - - offsetX = offsetX + finalRect.X + margin.Left; - offsetY = offsetY + finalRect.Y + margin.Top; - - if (GetUseLayoutRounding()) - { - offsetX = LayoutRound(offsetX); - offsetY = LayoutRound(offsetY); - } - } - //else - //{ - // offsetX = finalRect.X; - // offsetY = finalRect.Y; - //} - - NeedsClipToSlot = needsClipBounds; - -#if __WASM__ - if (FeatureConfiguration.UIElement.AssignDOMXamlProperties) - { - UpdateDOMXamlProperty(nameof(NeedsClipToSlot), NeedsClipToSlot); - } -#endif - var visualOffset = new Point(offsetX, offsetY); - var clippedFrame = GetClipRect(needsClipBounds, visualOffset, finalRect, new Size(maxWidth, maxHeight), margin); - ArrangeNative(visualOffset, clippedFrame); - - if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) - { - this.Log().LogWarning($"[LayoutCycleTracing] Arranged {this},{this.GetDebugName()}: {clippedFrame}."); - } - - AfterArrange(); - } - - internal virtual void AfterArrange() { } - - // Part of this code originates from https://github.com/dotnet/wpf/blob/b9b48871d457fc1f78fa9526c0570dae8e34b488/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/FrameworkElement.cs#L4877 - private protected virtual Rect? GetClipRect(bool needsClipToSlot, Point visualOffset, Rect finalRect, Size maxSize, Thickness margin) - { - if (needsClipToSlot) - { - Rect clipRect = default; - - // TODO: Clip rect currently only rounded in plateau > 1 to minimize impact, but should be consistently rounded in all plateaus. - var scale = RootScale.GetRasterizationScaleForElement(this); - var roundClipRect = (scale != 1.0f) && GetUseLayoutRounding(); - - var maxWidth = maxSize.Width; - var maxHeight = maxSize.Height; - - // this is in element's local rendering coord system - var inkSize = RenderSize; - var layoutSlotSize = finalRect.Size; - - var maxWidthClip = maxSize.Width.FiniteOrDefault(inkSize.Width); - var maxHeightClip = maxSize.Height.FiniteOrDefault(inkSize.Height); - - bool needToClipLocally; - - Size clippingSize = default; - - EnsureLayoutStorage(); - - // If clipping is forced, ensure the clip is at least as small as the RenderSize. - //if (forceClipToRenderSize) - //{ - // maxWidthClip = MIN(inkSize.width, maxWidthClip); - // maxHeightClip = MIN(inkSize.height, maxHeightClip); - // needToClipLocally = TRUE; - //} - //else - { - // need to clip if the computed sizes exceed MaxWidth/MaxHeight/Width/Height - needToClipLocally = IsLessThanAndNotCloseTo(maxWidthClip, inkSize.Width) - || IsLessThanAndNotCloseTo(maxHeightClip, inkSize.Height); - } - - // now lets say we already clipped by MaxWidth/MaxHeight, lets see if further clipping is needed - inkSize.Width = Math.Min(inkSize.Width, maxWidth); - inkSize.Height = Math.Min(inkSize.Height, maxHeight); - - // now lets say we already clipped by MaxWidth/MaxHeight, lets see if further clipping is needed - inkSize.Width = Math.Min(inkSize.Width, maxWidth); - inkSize.Height = Math.Min(inkSize.Height, maxHeight); - - //now see if layout slot should clip the element - var marginWidth = margin.Left + margin.Right; - var marginHeight = margin.Top + margin.Bottom; - - clippingSize.Width = Math.Max(0, layoutSlotSize.Width - marginWidth); - clippingSize.Height = Math.Max(0, layoutSlotSize.Height - marginHeight); - - // With layout rounding, MinMax and RenderSize are rounded. Clip size should be rounded as well. - if (roundClipRect) - { - clippingSize.Width = LayoutRound(clippingSize.Width); - clippingSize.Height = LayoutRound(clippingSize.Height); - } - - bool needToClipSlot = IsLessThanAndNotCloseTo(clippingSize.Width, inkSize.Width) - || IsLessThanAndNotCloseTo(clippingSize.Height, inkSize.Height); - - - if (needToClipSlot) - { - // The layout clip is created from the slot size determined in the parent's coordinate space, - // but is set on the child, meaning it's affected by the child's transform/offset and is applied in - // the child's coordinate space. The inverse of the offset is applied to the clip to prevent the clip's - // position from shifting as a result of the change in coordinates. - var offset = LayoutHelper.GetAlignmentOffset(this, clippingSize, inkSize); - var offsetX = offset.X; - var offsetY = offset.Y; - if (roundClipRect) - { - offsetX = LayoutRound(offsetX); - offsetY = LayoutRound(offsetY); - } - - clipRect = new Rect(-offsetX, -offsetY, clippingSize.Width, clippingSize.Height); - if (needToClipLocally) - { - clipRect = clipRect.IntersectWith(new Rect(0, 0, maxWidthClip, maxHeightClip)) ?? Rect.Empty; - } - } - else if (needToClipLocally) - { - // In this case clipRect starts at 0, 0 and max width/height clips are rounded due - // to RenderSize and MinMax being rounded. So clipRect is already rounded. - clipRect.Width = maxWidthClip; - clipRect.Height = maxHeightClip; - } - - // if we have difference between child and parent FlowDirection - // then we have to change origin of Clipping rectangle - // which allows us to visually keep it at the same place - // UNO TODO - //pParent = GetUIElementAdjustedParentInternal(FALSE /*fPublicParentsOnly*/); - //if (pParent && pParent->IsRightToLeft() != IsRightToLeft()) - //{ - // clipRect.X = RenderSize.width - (clipRect.X + clipRect.Width); - //} - - if (needToClipSlot || needToClipLocally) - { - if (ShouldApplyLayoutClipAsAncestorClip() -#if __WASM__ - && RenderTransform is { } renderTransform -#endif - ) - { -#if __SKIA__ - clipRect.X += visualOffset.X; - clipRect.Y += visualOffset.Y; -#elif __WASM__ - clipRect.X -= renderTransform.MatrixCore.M31; - clipRect.Y -= renderTransform.MatrixCore.M32; -#endif - } - - return clipRect; - } - } - - return null; - } - - /// - /// Calculates and applies native arrange properties. - /// - /// Offset of the view from its parent - /// Zone to clip, if clipping is required - private void ArrangeNative(Point offset, Rect? clippedFrame) - { - var newRect = new Rect(offset, RenderSize); - - if ( - newRect.Width < 0 - || newRect.Height < 0 - || double.IsNaN(newRect.Width) - || double.IsNaN(newRect.Height) - || double.IsNaN(newRect.X) - || double.IsNaN(newRect.Y) - ) - { - throw new InvalidOperationException($"{FormatDebugName()}: Invalid frame size {newRect}. No dimension should be NaN or negative value."); - } - -#if __SKIA__ - // clippedFrame here is the one calculated by FrameworkElement.GetClipRect - // which propagates to ShapeVisual.ViewBox. - // The UIElement.Clip public property isn't considered here on Skia because - // it's propagated to Visual.Clip and is set when UIElement.Clip changes. - ArrangeVisual(newRect, clippedFrame); -#else - var clip = Clip; - var clipRect = clip?.Rect; - if (clipRect.HasValue && clip?.Transform is { } transform) - { - clipRect = transform.TransformBounds(clipRect.Value); - } - - if (clipRect.HasValue || clippedFrame.HasValue) - { - clipRect = (clipRect ?? Rect.Infinite).IntersectWith(clippedFrame ?? Rect.Infinite); - } - - _logDebug?.Trace($"{DepthIndentation}{FormatDebugName()}.ArrangeElementNative({newRect}, clip={clipRect} (NeedsClipToSlot={NeedsClipToSlot})"); - - ArrangeVisual(newRect, clipRect); -#endif - } - internal override void EnterImpl(EnterParams @params, int depth) { var core = this.GetContext(); diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.cs new file mode 100644 index 000000000000..281edf02472b --- /dev/null +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.cs @@ -0,0 +1,790 @@ +#if !__NETSTD_REFERENCE__ +#nullable enable +using System; +using System.Globalization; +using System.Linq; +using Uno.Diagnostics.Eventing; +using Uno.Extensions; +using Uno.Foundation.Logging; +using Windows.Foundation; +using Microsoft.UI.Xaml.Controls.Primitives; + +using Uno.UI; +using Uno.UI.Xaml; +using static System.Math; +using static Uno.UI.LayoutHelper; +using Microsoft.UI.Xaml.Controls; +using Uno.UI.Xaml.Core; +using Uno.UI.Xaml.Core.Scaling; +using Uno.UI.Extensions; + +namespace Microsoft.UI.Xaml +{ + public partial class FrameworkElement + { + private const double SIZE_EPSILON = 0.05d; + private readonly static IEventProvider _trace = Tracing.Get(FrameworkElement.TraceProvider.Id); + + internal sealed override void MeasureCore(Size availableSize) + { + if (_trace.IsEnabled) + { + /// + /// This method contains or is called by a try/catch containing method and + /// can be significantly slower than other methods as a result on WebAssembly. + /// See https://github.com/dotnet/runtime/issues/56309 + /// + void MeasureCoreWithTrace(Size availableSize) + { + var traceActivity = _trace.WriteEventActivity( + TraceProvider.FrameworkElement_MeasureStart, + TraceProvider.FrameworkElement_MeasureStop, + new object[] { GetType().Name, this.GetDependencyObjectId(), Name, availableSize.ToString() } + ); + + using (traceActivity) + { + InnerMeasureCore(availableSize); + } + } + + MeasureCoreWithTrace(availableSize); + } + else + { + // This method is split in two functions to avoid the dynCalls + // invocations generation for mono-wasm AOT inside of try/catch/finally blocks. + InnerMeasureCore(availableSize); + } + + } + + private void InnerMeasureCore(Size availableSize) + { + if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().LogWarning($"[LayoutCycleTracing] Measuring {this},{this.GetDebugName()} with availableSize {availableSize}."); + } + + // Uno TODO + //CLayoutManager* pLayoutManager = VisualTree::GetLayoutManagerForElement(this); + //bool bInLayoutTransition = pLayoutManager ? pLayoutManager->GetTransitioningElement() == this : false; + + Size frameworkAvailableSize = default; + double minWidth = 0.0f; + double maxWidth = 0.0f; + double minHeight = 0.0f; + double maxHeight = 0.0f; + + double clippedDesiredWidth; + double clippedDesiredHeight; + + double marginWidth = 0.0f; + double marginHeight = 0.0f; + + //bool bTemplateApplied = false; + +#if UNO_HAS_ENHANCED_LIFECYCLE + RaiseLoadingEventIfNeeded(); +#endif + + //if (!bInLayoutTransition) + { + // Templates should be applied here. + //bTemplateApplied = InvokeApplyTemplate(); + + // TODO: BEGIN Uno specific + if (this is Control thisAsControl) + { + thisAsControl.ApplyTemplate(); + + // Update bindings to ensure resources defined + // in visual parents get applied. + this.UpdateResourceBindings(); + } + // TODO: END Uno specific + + // Subtract the margins from the available size + var margin = Margin; + marginWidth = margin.Left + margin.Right; + marginHeight = margin.Top + margin.Bottom; + + // We check to see if availableSize.width and availableSize.height are finite since that will + // also protect against NaN getting in. + frameworkAvailableSize.Width = double.IsFinite(availableSize.Width) ? Math.Max(availableSize.Width - marginWidth, 0) : double.PositiveInfinity; + frameworkAvailableSize.Height = double.IsFinite(availableSize.Height) ? Math.Max(availableSize.Height - marginHeight, 0) : double.PositiveInfinity; + + // Layout transforms would get processed here. + + // Adjust available size by Min/Max Width/Height + + var (minSize, maxSize) = this.GetMinMax(); + minWidth = minSize.Width; + minHeight = minSize.Height; + maxWidth = maxSize.Width; + maxHeight = maxSize.Height; + + frameworkAvailableSize.Width = Math.Max(minWidth, Math.Min(frameworkAvailableSize.Width, maxWidth)); + frameworkAvailableSize.Height = Math.Max(minHeight, Math.Min(frameworkAvailableSize.Height, maxHeight)); + } + //else + //{ + // // when in a transition, just take the passed in constraint without considering the above + // frameworkAvailableSize = availableSize; + //} + + var desiredSize = MeasureOverride(frameworkAvailableSize); + + // We need to round now since we save the values off, and use them to determine + // if a layout clip will be applied. + if (GetUseLayoutRounding()) + { + desiredSize.Width = LayoutRound(desiredSize.Width); + desiredSize.Height = LayoutRound(desiredSize.Height); + + } + + //if (!bInLayoutTransition) + { + // Maximize desired size with user provided min size. It's also possible that MeasureOverride returned NaN for either + // width or height, in which case we should use the min size as well. + desiredSize.Width = Math.Max(desiredSize.Width, minWidth); + if (double.IsNaN(desiredSize.Width)) + { + desiredSize.Width = minWidth; + } + desiredSize.Height = Math.Max(desiredSize.Height, minHeight); + if (double.IsNaN(desiredSize.Height)) + { + desiredSize.Height = minHeight; + } + + // We need to round now since we save the values off, and use them to determine + // if a layout clip will be applied. + + if (GetUseLayoutRounding()) + { + desiredSize.Width = LayoutRound(desiredSize.Width); + desiredSize.Height = LayoutRound(desiredSize.Height); + } + + // Here is the "true minimum" desired size - the one that is + // for sure enough for the control to render its content. + EnsureLayoutStorage(); + m_unclippedDesiredSize = desiredSize; + + // More layout transforms processing here. + + if (desiredSize.Width > maxWidth) + { + desiredSize.Width = maxWidth; + } + + if (desiredSize.Height > maxHeight) + { + desiredSize.Height = maxHeight; + } + + // Transform desired size to layout slot space (placeholder for when we do layout transforms) + + // Layout round the margins too. This corresponds to the behavior in ArrangeCore, where we check the unclipped desired + // size against available space minus the rounded margin. This also prevents a bug where MeasureCore adds the unrounded + // margin (e.g. 14) to the desired size (e.g. 55.56) and rounds the final result (69.56 rounded to 69.33 under 2.25x scale), + // then ArrangeCore takes that rounded result (i.e. 69.33), subtracts the unrounded margin (i.e. 14) and ends up with a + // size smaller than the desired size (69.33 - 14 = 55.33 < 55.56). This ends up putting a layout clip on an element that + // doesn't need one, and causes big problems if the element is the scrollable extent of a carousel panel. + double roundedMarginWidth = marginWidth; + double roundedMarginHeight = marginHeight; + if (GetUseLayoutRounding()) + { + roundedMarginWidth = LayoutRound(marginWidth); + roundedMarginHeight = LayoutRound(marginHeight); + } + + // Because of negative margins, clipped desired size may be negative. + // Need to keep it as XFLOATS for that reason and maximize with 0 at the + // very last point - before returning desired size to the parent. + clippedDesiredWidth = desiredSize.Width + roundedMarginWidth; + clippedDesiredHeight = desiredSize.Height + roundedMarginHeight; + + // only clip and constrain if the tree wants that. + // currently only listviewitems do not want clipping + // UNO TODO + + //if (!pLayoutManager->GetIsInNonClippingTree()) + { + // In overconstrained scenario, parent wins and measured size of the child, + // including any sizes set or computed, can not be larger then + // available size. We will clip the guy later. + if (clippedDesiredWidth > availableSize.Width) + { + clippedDesiredWidth = availableSize.Width; + } + + if (clippedDesiredHeight > availableSize.Height) + { + clippedDesiredHeight = availableSize.Height; + } + } + + // Note: unclippedDesiredSize is needed in ArrangeCore, + // because due to the layout protocol, arrange should be called + // with constraints greater or equal to child's desired size + // returned from MeasureOverride. But in most circumstances + // it is possible to reconstruct original unclipped desired size. + + desiredSize.Width = Math.Max(0, clippedDesiredWidth); + desiredSize.Height = Math.Max(0, clippedDesiredHeight); + } + //else + //{ + // // in LT, need to take precautions + // desiredSize.Width = Math.Max(desiredSize.Width, 0.0f); + // desiredSize.Height = Math.Max(desiredSize.Height, 0.0f); + //} + + // We need to round again in case the desired size has been modified since we originally + // rounded it. + if (GetUseLayoutRounding()) + { + desiredSize.Width = LayoutRound(desiredSize.Width); + desiredSize.Height = LayoutRound(desiredSize.Height); + } + + if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().LogWarning($"[LayoutCycleTracing] Measured {this},{this.GetDebugName()}: desiredSize is {desiredSize}."); + } + +#if __SKIA__ + if (desiredSize != DesiredSize) +#endif + { + // DesiredSize must include margins + m_desiredSize = desiredSize; + +#if __SKIA__ + this.OnDesiredSizeChanged(); +#endif + } + } + + private protected virtual void OnDesiredSizeChanged() + { + + } + + internal sealed override void ArrangeCore(Rect finalRect) + { + if (_trace.IsEnabled) + { + void ArrangeCoreWithTrace(Rect finalRect) + { + var traceActivity = _trace.WriteEventActivity( + TraceProvider.FrameworkElement_ArrangeStart, + TraceProvider.FrameworkElement_ArrangeStop, + new object[] { GetType().Name, this.GetDependencyObjectId(), Name, finalRect.ToString() } + ); + + using (traceActivity) + { + InnerArrangeCore(finalRect); + } + } + + ArrangeCoreWithTrace(finalRect); + } + else + { + // This method is split in two functions to avoid the dynCalls + // invocations generation for mono-wasm AOT inside of try/catch/finally blocks. + InnerArrangeCore(finalRect); + } + + } + + private static bool IsLessThanAndNotCloseTo(double a, double b) => a < (b - SIZE_EPSILON); + + private void InnerArrangeCore(Rect finalRect) + { + if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().LogWarning($"[LayoutCycleTracing] Arranging {this},{this.GetDebugName()} with finalRect {finalRect}."); + } + + // Uno TODO: + //CLayoutManager* pLayoutManager = VisualTree::GetLayoutManagerForElement(this); + //bool bInLayoutTransition = pLayoutManager ? pLayoutManager->GetTransitioningElement() == this : false; + + bool needsClipBounds = false; + + var arrangeSize = finalRect.Size; + + var margin = Margin; + var marginWidth = margin.Left + margin.Right; + var marginHeight = margin.Top + margin.Bottom; + + var ha = HorizontalAlignment; + var va = VerticalAlignment; + + Size unclippedDesiredSize = default; + double minWidth = 0, maxWidth = 0, minHeight = 0, maxHeight = 0; + double effectiveMaxWidth = 0, effectiveMaxHeight = 0; + + Size oldRenderSize = default; + Size innerInkSize = default; + Size clippedInkSize = default; + Size clientSize = default; + double offsetX = 0, offsetY = 0; + + EnsureLayoutStorage(); + + unclippedDesiredSize = m_unclippedDesiredSize; + oldRenderSize = RenderSize; + + //if (!bInLayoutTransition) + { + Size arrangeSizeWithoutMargin = new Size( + Math.Max(arrangeSize.Width - marginWidth, 0), + Math.Max(arrangeSize.Height - marginHeight, 0) + ); + + var roundedMarginWidth = marginWidth; + var roundedMarginHeight = marginHeight; + + if (GetUseLayoutRounding()) + { + roundedMarginWidth = LayoutRound(marginWidth); + roundedMarginHeight = LayoutRound(marginHeight); + } + + // We handle layout rounding inconsistently across our code, this change is restricted to using layout + // rounding for margin only on certain scenarios to avoid introducing unexpected problems. + // If further rounding issues appear we should consider opening this behaviour to more scenarios. + if (roundedMarginWidth != marginWidth && arrangeSizeWithoutMargin.Width != unclippedDesiredSize.Width) + { + double arrangeWidthWithoutRoundedMargin = Math.Max(arrangeSize.Width - roundedMarginWidth, 0); + if (arrangeWidthWithoutRoundedMargin == unclippedDesiredSize.Width) + { + // The rounding difference between arrangeSizeWithoutMargin.width and unclippedDesiredSize.width + // comes from the horizontal margin. The rounded value of that margin must be used so that this + // FrameworkElement's ActualWidth does not return an incorrect value. + marginWidth = roundedMarginWidth; + arrangeSize.Width = arrangeWidthWithoutRoundedMargin; + } + else + { + arrangeSize.Width = arrangeSizeWithoutMargin.Width; + } + } + else + { + arrangeSize.Width = arrangeSizeWithoutMargin.Width; + } + + if (roundedMarginHeight != marginHeight && arrangeSizeWithoutMargin.Height != unclippedDesiredSize.Height) + { + double arrangeHeightWithoutRoundedMargin = Math.Max(arrangeSize.Height - roundedMarginHeight, 0); + if (arrangeHeightWithoutRoundedMargin == unclippedDesiredSize.Height) + { + // The rounding difference between arrangeSizeWithoutMargin.height and unclippedDesiredSize.height + // comes from the vertical margin. The rounded value of that margin must be used so that this + // FrameworkElement's ActualHeight does not return an incorrect value. + marginHeight = roundedMarginHeight; + arrangeSize.Height = arrangeHeightWithoutRoundedMargin; + } + else + { + arrangeSize.Height = arrangeSizeWithoutMargin.Height; + } + } + else + { + arrangeSize.Height = arrangeSizeWithoutMargin.Height; + } + + if (IsLessThanAndNotCloseTo(arrangeSize.Width, unclippedDesiredSize.Width)) + { + needsClipBounds = true; + arrangeSize.Width = unclippedDesiredSize.Width; + } + + if (IsLessThanAndNotCloseTo(arrangeSize.Height, unclippedDesiredSize.Height)) + { + needsClipBounds = true; + arrangeSize.Height = unclippedDesiredSize.Height; + } + + // Alignment==Stretch --> arrange at the slot size minus margins + // Alignment!=Stretch --> arrange at the unclippedDesiredSize + if (ha != HorizontalAlignment.Stretch) + { + arrangeSize.Width = unclippedDesiredSize.Width; + } + + if (va != VerticalAlignment.Stretch) + { + arrangeSize.Height = unclippedDesiredSize.Height; + } + + var (minSize, maxSize) = this.GetMinMax(); + minWidth = minSize.Width; + maxWidth = maxSize.Width; + minHeight = minSize.Height; + maxHeight = maxSize.Height; + + // Layout transforms processed here + + // We have to choose max between UnclippedDesiredSize and Max here, because + // otherwise setting of max property could cause arrange at less then unclippedDS. + // Clipping by Max is needed to limit stretch here + + effectiveMaxWidth = Math.Max(unclippedDesiredSize.Width, maxWidth); + if (IsLessThanAndNotCloseTo(effectiveMaxWidth, arrangeSize.Width)) + { + needsClipBounds = true; + arrangeSize.Width = effectiveMaxWidth; + } + + effectiveMaxHeight = Math.Max(unclippedDesiredSize.Height, maxHeight); + if (IsLessThanAndNotCloseTo(effectiveMaxHeight, arrangeSize.Height)) + { + needsClipBounds = true; + arrangeSize.Height = effectiveMaxHeight; + } + } + + innerInkSize = ArrangeOverride(arrangeSize); + + // Here we use un-clipped InkSize because element does not know that it is + // clipped by layout system and it shoudl have as much space to render as + // it returned from its own ArrangeOverride + // Inner ink size is not guaranteed to be rounded, but should be. + // TODO: inner ink size currently only rounded if plateau > 1 to minimize impact in RC, + // but should be consistently rounded in all plateaus. + var scale = RootScale.GetRasterizationScaleForElement(this); + if ((scale != 1.0f) && GetUseLayoutRounding()) + { + innerInkSize.Width = LayoutRound(innerInkSize.Width); + innerInkSize.Height = LayoutRound(innerInkSize.Height); + } + RenderSize = innerInkSize; + + //if (!IsSameSize(oldRenderSize, innerInkSize)) + //{ + // OnActualSizeChanged(); + //} + +#if UNO_HAS_ENHANCED_LIFECYCLE + if (oldRenderSize != innerInkSize) + { + this.GetContext().EventManager.EnqueueForSizeChanged(this, oldRenderSize); + } +#endif + + //if (!bInLayoutTransition) + { + // ClippedInkSize differs from InkSize only what MaxWidth/Height explicitly clip the + // otherwise good arrangement. For ex, DSMaxWidth - in this + // case we should initiate clip at MaxWidth and only show Top-Left portion + // of the element limited by Max properties. It is Top-left because in case when we + // are clipped by container we also degrade to Top-Left, so we are consistent. + clippedInkSize.Width = Math.Min(innerInkSize.Width, maxWidth); + clippedInkSize.Height = Math.Min(innerInkSize.Height, maxHeight); + + // remember we have to clip if Max properties limit the inkSize + needsClipBounds |= + IsLessThanAndNotCloseTo(clippedInkSize.Width, innerInkSize.Width) + || IsLessThanAndNotCloseTo(clippedInkSize.Height, innerInkSize.Height); + + // Transform stuff here + + // Note that inkSize now can be bigger then layoutSlotSize-margin (because of layout + // squeeze by the parent or LayoutConstrained=true, which clips desired size in Measure). + + // The client size is the size of layout slot decreased by margins. + // This is the "window" through which we see the content of the child. + // Alignments position ink of the child in this "window". + // Max with 0 is necessary because layout slot may be smaller then unclipped desired size. + clientSize.Width = Math.Max(0, finalRect.Width - marginWidth); + clientSize.Height = Math.Max(0, finalRect.Height - marginHeight); + + // Remember we have to clip if clientSize limits the inkSize + needsClipBounds |= + IsLessThanAndNotCloseTo(clientSize.Width, clippedInkSize.Width) + || IsLessThanAndNotCloseTo(clientSize.Height, clippedInkSize.Height); + + //bool isAlignedByDirectManipulation = IsAlignedByDirectManipulation(); + + //if (isAlignedByDirectManipulation) + //{ + // // Skip the layout engine's contribution to the element's offsets when it is already aligned by DirectManipulation. + + // if (m_pLayoutProperties.m_horizontalAlignment == HorizontalAlignment.Stretch) + // { + // // Check if the Stretch alignment needs to be overridden with a Left alignment. + // // The "IsStretchHorizontalAlignmentTreatedAsLeft" case corresponds to CFrameworkElement::ComputeAlignmentOffset's "degenerate Stretch to Top-Left" branch. + // // The "IsFinalArrangeSizeMaximized()" case is for text controls CTextBlock, CRichTextBlock and CRichTextBlockOverflow which stretch their desired width to the finalSize argument in their ArrangeOverride method. + // // The "(clippedInkSize.width == clientSize.width && unclippedDesiredSize.width < clientSize.width)" case is for 3rd party controls that stretch their desired width to the final arrange width too. + // bool isStretchAlignmentTreatedAsNear_New = + // IsStretchHorizontalAlignmentTreatedAsLeft(HorizontalAlignment.Stretch, clientSize, clippedInkSize) || + // (clippedInkSize.Width == clientSize.Width && unclippedDesiredSize.Width < clientSize.Width) || + // IsFinalArrangeSizeMaximized(); + + // // Check if the overriding needs are changing by accessing the current status from the owning ScrollViewer control. + // bool isStretchAlignmentTreatedAsNear_Old = IsStretchAlignmentTreatedAsNear(true /*isForHorizontalAlignment*/); + // if (isStretchAlignmentTreatedAsNear_New != isStretchAlignmentTreatedAsNear_Old) + // { + // // The overriding needs are changing - push the new status to the owning ScrollViewer control. + // OnAlignmentChanged(true /*fIsForHorizontalAlignment*/, true/*fIsForStretchAlignment*/, isStretchAlignmentTreatedAsNear_New); + // } + // } + + // if (m_pLayoutProperties.m_verticalAlignment == VerticalAlignment.Stretch) + // { + // // Check if the Stretch alignment needs to be overridden with a Top alignment. + // // The "IsStretchVerticalAlignmentTreatedAsTop" case corresponds to CFrameworkElement::ComputeAlignmentOffset's "degenerate Stretch to Top-Left" branch. + // // The "IsFinalArrangeSizeMaximized()" case is for text controls CTextBlock, CRichTextBlock and CRichTextBlockOverflow which stretch their desired height to the finalSize argument in their ArrangeOverride method. + // // The "(clippedInkSize.height == clientSize.height && unclippedDesiredSize.height < clientSize.height)" case is for 3rd party controls that stretch their desired height to the final arrange height too. + // bool isStretchAlignmentTreatedAsNear_New = + // IsStretchVerticalAlignmentTreatedAsTop(VerticalAlignment.Stretch, clientSize, clippedInkSize) || + // (clippedInkSize.Height == clientSize.Height && unclippedDesiredSize.Height < clientSize.Height) || + // IsFinalArrangeSizeMaximized(); + + // // Check if the overriding needs are changing by accessing the current status from the owning ScrollViewer control. + // bool isStretchAlignmentTreatedAsNear_Old = IsStretchAlignmentTreatedAsNear(false /*isForHorizontalAlignment*/); + // if (isStretchAlignmentTreatedAsNear_New != isStretchAlignmentTreatedAsNear_Old) + // { + // // The overriding needs are changing - push the new status to the owning ScrollViewer control. + // OnAlignmentChanged(false /*fIsForHorizontalAlignment*/, true /*fIsForStretchAlignment*/, isStretchAlignmentTreatedAsNear_New); + // } + // } + //} + //else + { + var offset = this.GetAlignmentOffset(clientSize, clippedInkSize); + offsetX = offset.X; + offsetY = offset.Y; + } + + //oldOffset = VisualOffset; + + //VisualOffset.x = offsetX + finalRect.X + m_pLayoutProperties->m_margin.left; + //VisualOffset.y = offsetY + finalRect.Y + m_pLayoutProperties->m_margin.top; + + offsetX = offsetX + finalRect.X + margin.Left; + offsetY = offsetY + finalRect.Y + margin.Top; + + if (GetUseLayoutRounding()) + { + offsetX = LayoutRound(offsetX); + offsetY = LayoutRound(offsetY); + } + } + //else + //{ + // offsetX = finalRect.X; + // offsetY = finalRect.Y; + //} + + NeedsClipToSlot = needsClipBounds; + +#if __WASM__ + if (FeatureConfiguration.UIElement.AssignDOMXamlProperties) + { + UpdateDOMXamlProperty(nameof(NeedsClipToSlot), NeedsClipToSlot); + } +#endif + var visualOffset = new Point(offsetX, offsetY); + var clippedFrame = GetClipRect(needsClipBounds, visualOffset, finalRect, new Size(maxWidth, maxHeight), margin); + ArrangeNative(visualOffset, clippedFrame); + + if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().LogWarning($"[LayoutCycleTracing] Arranged {this},{this.GetDebugName()}: {clippedFrame}."); + } + + AfterArrange(); + } + + internal virtual void AfterArrange() { } + + // Part of this code originates from https://github.com/dotnet/wpf/blob/b9b48871d457fc1f78fa9526c0570dae8e34b488/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/FrameworkElement.cs#L4877 + private protected virtual Rect? GetClipRect(bool needsClipToSlot, Point visualOffset, Rect finalRect, Size maxSize, Thickness margin) + { + if (needsClipToSlot) + { + Rect clipRect = default; + + // TODO: Clip rect currently only rounded in plateau > 1 to minimize impact, but should be consistently rounded in all plateaus. + var scale = RootScale.GetRasterizationScaleForElement(this); + var roundClipRect = (scale != 1.0f) && GetUseLayoutRounding(); + + var maxWidth = maxSize.Width; + var maxHeight = maxSize.Height; + + // this is in element's local rendering coord system + var inkSize = RenderSize; + var layoutSlotSize = finalRect.Size; + + var maxWidthClip = maxSize.Width.FiniteOrDefault(inkSize.Width); + var maxHeightClip = maxSize.Height.FiniteOrDefault(inkSize.Height); + + bool needToClipLocally; + + Size clippingSize = default; + + EnsureLayoutStorage(); + + // If clipping is forced, ensure the clip is at least as small as the RenderSize. + //if (forceClipToRenderSize) + //{ + // maxWidthClip = MIN(inkSize.width, maxWidthClip); + // maxHeightClip = MIN(inkSize.height, maxHeightClip); + // needToClipLocally = TRUE; + //} + //else + { + // need to clip if the computed sizes exceed MaxWidth/MaxHeight/Width/Height + needToClipLocally = IsLessThanAndNotCloseTo(maxWidthClip, inkSize.Width) + || IsLessThanAndNotCloseTo(maxHeightClip, inkSize.Height); + } + + // now lets say we already clipped by MaxWidth/MaxHeight, lets see if further clipping is needed + inkSize.Width = Math.Min(inkSize.Width, maxWidth); + inkSize.Height = Math.Min(inkSize.Height, maxHeight); + + // now lets say we already clipped by MaxWidth/MaxHeight, lets see if further clipping is needed + inkSize.Width = Math.Min(inkSize.Width, maxWidth); + inkSize.Height = Math.Min(inkSize.Height, maxHeight); + + //now see if layout slot should clip the element + var marginWidth = margin.Left + margin.Right; + var marginHeight = margin.Top + margin.Bottom; + + clippingSize.Width = Math.Max(0, layoutSlotSize.Width - marginWidth); + clippingSize.Height = Math.Max(0, layoutSlotSize.Height - marginHeight); + + // With layout rounding, MinMax and RenderSize are rounded. Clip size should be rounded as well. + if (roundClipRect) + { + clippingSize.Width = LayoutRound(clippingSize.Width); + clippingSize.Height = LayoutRound(clippingSize.Height); + } + + bool needToClipSlot = IsLessThanAndNotCloseTo(clippingSize.Width, inkSize.Width) + || IsLessThanAndNotCloseTo(clippingSize.Height, inkSize.Height); + + + if (needToClipSlot) + { + // The layout clip is created from the slot size determined in the parent's coordinate space, + // but is set on the child, meaning it's affected by the child's transform/offset and is applied in + // the child's coordinate space. The inverse of the offset is applied to the clip to prevent the clip's + // position from shifting as a result of the change in coordinates. + var offset = LayoutHelper.GetAlignmentOffset(this, clippingSize, inkSize); + var offsetX = offset.X; + var offsetY = offset.Y; + if (roundClipRect) + { + offsetX = LayoutRound(offsetX); + offsetY = LayoutRound(offsetY); + } + + clipRect = new Rect(-offsetX, -offsetY, clippingSize.Width, clippingSize.Height); + if (needToClipLocally) + { + clipRect = clipRect.IntersectWith(new Rect(0, 0, maxWidthClip, maxHeightClip)) ?? Rect.Empty; + } + } + else if (needToClipLocally) + { + // In this case clipRect starts at 0, 0 and max width/height clips are rounded due + // to RenderSize and MinMax being rounded. So clipRect is already rounded. + clipRect.Width = maxWidthClip; + clipRect.Height = maxHeightClip; + } + + // if we have difference between child and parent FlowDirection + // then we have to change origin of Clipping rectangle + // which allows us to visually keep it at the same place + // UNO TODO + //pParent = GetUIElementAdjustedParentInternal(FALSE /*fPublicParentsOnly*/); + //if (pParent && pParent->IsRightToLeft() != IsRightToLeft()) + //{ + // clipRect.X = RenderSize.width - (clipRect.X + clipRect.Width); + //} + + if (needToClipSlot || needToClipLocally) + { + if (ShouldApplyLayoutClipAsAncestorClip() +#if __WASM__ + && RenderTransform is { } renderTransform +#endif + ) + { +#if __SKIA__ + clipRect.X += visualOffset.X; + clipRect.Y += visualOffset.Y; +#elif __WASM__ + clipRect.X -= renderTransform.MatrixCore.M31; + clipRect.Y -= renderTransform.MatrixCore.M32; +#endif + } + + return clipRect; + } + } + + return null; + } + + /// + /// Calculates and applies native arrange properties. + /// + /// Offset of the view from its parent + /// Zone to clip, if clipping is required + private void ArrangeNative(Point offset, Rect? clippedFrame) + { + var newRect = new Rect(offset, RenderSize); + + if ( + newRect.Width < 0 + || newRect.Height < 0 + || double.IsNaN(newRect.Width) + || double.IsNaN(newRect.Height) + || double.IsNaN(newRect.X) + || double.IsNaN(newRect.Y) + ) + { + throw new InvalidOperationException($"{FormatDebugName()}: Invalid frame size {newRect}. No dimension should be NaN or negative value."); + } + +#if __SKIA__ + // clippedFrame here is the one calculated by FrameworkElement.GetClipRect + // which propagates to ShapeVisual.ViewBox. + // The UIElement.Clip public property isn't considered here on Skia because + // it's propagated to Visual.Clip and is set when UIElement.Clip changes. + ArrangeVisual(newRect, clippedFrame); +#else + var clip = Clip; + var clipRect = clip?.Rect; + if (clipRect.HasValue && clip?.Transform is { } transform) + { + clipRect = transform.TransformBounds(clipRect.Value); + } + + if (clipRect.HasValue || clippedFrame.HasValue) + { + clipRect = (clipRect ?? Rect.Infinite).IntersectWith(clippedFrame ?? Rect.Infinite); + } + + ArrangeVisual(newRect, clipRect); +#endif + } + + private string FormatDebugName() + => $"[{this}/{Name}"; + } +} +#endif diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.cs index 4b8351833a18..6c8a17b4db49 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.cs @@ -29,7 +29,9 @@ using Uno.UI.Xaml.Core; using Uno.UI.Xaml.Media; + #if __ANDROID__ +using Android.Views; using View = Android.Views.View; #elif __IOS__ using View = UIKit.UIView; @@ -46,9 +48,6 @@ namespace Microsoft.UI.Xaml { public partial class FrameworkElement : UIElement, IFrameworkElement, IFrameworkElementInternal, ILayoutConstraints, IDependencyObjectParse -#if !UNO_REFERENCE_API - , ILayouterElement -#endif { public static class TraceProvider { @@ -61,16 +60,6 @@ public static class TraceProvider public const int FrameworkElement_InvalidateMeasure = 5; } -#if !UNO_REFERENCE_API - private FrameworkElementLayouter _layouter; - - ILayouter ILayouterElement.Layouter => _layouter; - Size ILayouterElement.LastAvailableSize => m_previousAvailableSize; - bool ILayouterElement.IsMeasureDirty => IsMeasureDirty; - bool ILayouterElement.IsFirstMeasureDoneAndManagedElement => IsFirstMeasureDone; - bool ILayouterElement.IsMeasureDirtyPathDisabled => IsMeasureDirtyPathDisabled; -#endif - private bool _defaultStyleApplied; private static readonly Uri DefaultBaseUri = new Uri("ms-appx://local"); @@ -247,7 +236,7 @@ private double ComputeHeightInMinMaxRange(double height) partial void Initialize() { #if !UNO_REFERENCE_API - _layouter = new FrameworkElementLayouter(this, MeasureOverride, ArrangeOverride); + //_layouter = new FrameworkElementLayouter(this, MeasureOverride, ArrangeOverride); #endif Resources = new Microsoft.UI.Xaml.ResourceDictionary(); @@ -369,11 +358,43 @@ protected virtual Size ArrangeOverride(Size finalSize) /// The measured size - INCLUDES THE MARGIN protected Size MeasureElement(View view, Size availableSize) { -#if UNO_REFERENCE_API +#if __CROSSRUNTIME__ || IS_UNIT_TESTS view.Measure(availableSize); return view.DesiredSize; #else - return _layouter.MeasureElement(view, availableSize); + if (view is UIElement viewAsUIElement) + { + viewAsUIElement.Measure(availableSize); + return viewAsUIElement.DesiredSize; + } + else if (view is ILayouterElement layouterElement) + { + return layouterElement.Measure(availableSize); + } + +#if __ANDROID__ + var widthSpec = ViewHelper.MakeMeasureSpec((int)availableSize.Width, Android.Views.MeasureSpecMode.AtMost); + var heightSpec = ViewHelper.MakeMeasureSpec((int)availableSize.Height, Android.Views.MeasureSpecMode.AtMost); + view.Measure(widthSpec, heightSpec); + + if (view is ViewGroup viewGroup) + { + var childCount = viewGroup.ChildCount; + for (int i = 0; i < childCount; i++) + { + MeasureElement(viewGroup.GetChildAt(i), availableSize); + } + } +#elif __IOS__ + foreach (var child in view.Subviews) + { + MeasureElement(child, availableSize); + } +#elif __MACOS + // TODO +#endif + + return availableSize; #endif } @@ -384,10 +405,42 @@ protected Size MeasureElement(View view, Size availableSize) /// The final size that the parent computes for the child in layout, provided as a value. protected void ArrangeElement(View view, Rect finalRect) { -#if UNO_REFERENCE_API +#if __CROSSRUNTIME__ view.Arrange(finalRect); #else - _layouter.ArrangeElement(view, finalRect); + if (view is UIElement viewAsUIElement) + { + viewAsUIElement.Arrange(finalRect); + } + else if (view is ILayouterElement layouterElement) + { + layouterElement.Arrange(finalRect); + } + else + { + var physicalRect = ViewHelper.LogicalToPhysicalPixels(finalRect); +#if __ANDROID__ + view.Layout((int)physicalRect.Left, (int)physicalRect.Top, (int)physicalRect.Right, (int)physicalRect.Bottom); + + if (view is ViewGroup viewGroup) + { + var childCount = viewGroup.ChildCount; + for (int i = 0; i < childCount; i++) + { + ArrangeElement(viewGroup.GetChildAt(i), finalRect); + } + } +#elif __IOS__ + view.Frame = physicalRect; + + foreach (var subview in view.Subviews) + { + ArrangeElement(subview, finalRect); + } +#elif __MACOS__ + // TODO +#endif + } #endif } @@ -396,8 +449,8 @@ protected void ArrangeElement(View view, Rect finalRect) /// protected Size GetElementDesiredSize(View view) { -#if UNO_REFERENCE_API - return view.DesiredSize; +#if true + return (view as UIElement)?.DesiredSize ?? default; // TODO #else return (_layouter as ILayouter).GetDesiredSize(view); #endif @@ -1019,34 +1072,34 @@ public AutomationPeer GetAutomationPeer() #endregion #if !UNO_REFERENCE_API - private class FrameworkElementLayouter : Layouter - { - private readonly MeasureOverrideHandler _measureOverrideHandler; - private readonly ArrangeOverrideHandler _arrangeOverrideHandler; + // private class FrameworkElementLayouter : Layouter + // { + // private readonly MeasureOverrideHandler _measureOverrideHandler; + // private readonly ArrangeOverrideHandler _arrangeOverrideHandler; - public delegate Size ArrangeOverrideHandler(Size finalSize); - public delegate Size MeasureOverrideHandler(Size availableSize); + // public delegate Size ArrangeOverrideHandler(Size finalSize); + // public delegate Size MeasureOverrideHandler(Size availableSize); - public FrameworkElementLayouter(IFrameworkElement element, MeasureOverrideHandler measureOverrideHandler, ArrangeOverrideHandler arrangeOverrigeHandler) : base(element) - { - _measureOverrideHandler = measureOverrideHandler; - _arrangeOverrideHandler = arrangeOverrigeHandler; - } + // public FrameworkElementLayouter(IFrameworkElement element, MeasureOverrideHandler measureOverrideHandler, ArrangeOverrideHandler arrangeOverrigeHandler) : base(element) + // { + // _measureOverrideHandler = measureOverrideHandler; + // _arrangeOverrideHandler = arrangeOverrigeHandler; + // } - public Size MeasureElement(View element, Size availableSize) => MeasureChild(element, availableSize); + // public Size MeasureElement(View element, Size availableSize) => MeasureChild(element, availableSize); - public void ArrangeElement(View element, Rect finalRect) => ArrangeChild(element, finalRect); + // public void ArrangeElement(View element, Rect finalRect) => ArrangeChild(element, finalRect); - protected override string Name => Panel.Name; + // protected override string Name => Panel.Name; - protected override Size ArrangeOverride(Size finalSize) => _arrangeOverrideHandler(finalSize); + // protected override Size ArrangeOverride(Size finalSize) => _arrangeOverrideHandler(finalSize); -#if __ANDROID__ - protected override void MeasureChild(View view, int widthSpec, int heightSpec) => view.Measure(widthSpec, heightSpec); -#endif + //#if __ANDROID__ + // protected override void MeasureChild(View view, int widthSpec, int heightSpec) => view.Measure(widthSpec, heightSpec); + //#endif - protected override Size MeasureOverride(Size availableSize) => _measureOverrideHandler(availableSize); - } + // protected override Size MeasureOverride(Size availableSize) => _measureOverrideHandler(availableSize); + // } #endif } } diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.iOS.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.iOS.cs index a9232004dffb..9f08938a878a 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.iOS.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.iOS.cs @@ -16,108 +16,19 @@ namespace Microsoft.UI.Xaml { public partial class FrameworkElement { - /// - /// When set, measure and invalidate requests will not be propagated further up the visual tree, ie they won't trigger a relayout. - /// Used where repeated unnecessary measure/arrange passes would be unacceptable for performance (eg scrolling in a list). - /// - internal bool ShouldInterceptInvalidate { get; set; } - public override void SetNeedsLayout() { - if (ShouldInterceptInvalidate) - { - return; - } - - if (!_inLayoutSubviews) - { - base.SetNeedsLayout(); - } - - SetLayoutFlags(LayoutFlag.MeasureDirty | LayoutFlag.ArrangeDirty); - - if (FeatureConfiguration.FrameworkElement.IOsAllowSuperviewNeedsLayoutWhileInLayoutSubViews || !_inLayoutSubviews) - { - SetSuperviewNeedsLayout(); - } + base.SetNeedsLayout(); } public override void LayoutSubviews() { - try - { - try - { - _inLayoutSubviews = true; - - if (IsMeasureDirty) - { - // Add back the Margin (which is normally 'outside' the view's bounds) - the layouter will subtract it again - var availableSizeWithMargins = Bounds.Size.Add(Margin); - XamlMeasure(availableSizeWithMargins); - } - - //if (IsArrangeDirty) // commented until the MEASURE_DIRTY_PATH is properly implemented for iOS - { - ClearLayoutFlags(LayoutFlag.ArrangeDirty); - - OnBeforeArrange(); - - Rect finalRect; - var parent = Superview; - if (parent is UIElement or ISetLayoutSlots) - { - finalRect = LayoutSlotWithMarginsAndAlignments; - } - else - { - // Here the "arrange" is coming from a native element, - // so we convert those measurements to logical ones. - finalRect = RectFromUIRect(Frame); - - // We also need to set the LayoutSlot as it was not by the parent. - // Note: This is only an approximation of the LayoutSlot as margin and alignment might already been applied at this point. - LayoutInformation.SetLayoutSlot(this, finalRect); - LayoutSlotWithMarginsAndAlignments = finalRect; - } - - _layouter.Arrange(finalRect); - - OnAfterArrange(); - } - } - finally - { - _inLayoutSubviews = false; - } - } - catch (Exception e) - { - this.Log().Error($"Layout failed in {GetType()}", e); - } + base.LayoutSubviews(); } public override CGSize SizeThatFits(CGSize size) { - try - { - _inLayoutSubviews = true; - - var xamlMeasure = XamlMeasure(size); - - if (xamlMeasure != null) - { - return _lastMeasure = xamlMeasure.Value; - } - else - { - return _lastMeasure = base.SizeThatFits(size); - } - } - finally - { - _inLayoutSubviews = false; - } + return base.SizeThatFits(size); } public override void AddSubview(UIView view) diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.iOSmacOS.cs index e67056244a95..d9304262a86c 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.iOSmacOS.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.iOSmacOS.cs @@ -39,18 +39,6 @@ partial void OnLoadedPartial() private partial void ReconfigureViewportPropagationPartial(); - internal CGSize? XamlMeasure(CGSize availableSize) - { - if (((ILayouterElement)this).XamlMeasureInternal(availableSize, _lastAvailableSize, out var measuredSize)) - { - _lastAvailableSize = availableSize; - _lastMeasure = measuredSize; - SetLayoutFlags(LayoutFlag.ArrangeDirty); - } - - return _lastMeasure; - } - /// /// Called before Arrange is called, this method will be deprecated /// once OnMeasure/OnArrange will be implemented completely diff --git a/src/Uno.UI/UI/Xaml/IFrameworkElement.cs b/src/Uno.UI/UI/Xaml/IFrameworkElement.cs index 0df45017df84..588078a0c14a 100644 --- a/src/Uno.UI/UI/Xaml/IFrameworkElement.cs +++ b/src/Uno.UI/UI/Xaml/IFrameworkElement.cs @@ -446,36 +446,6 @@ public static void MaybeOrNot(this TInstance instance, Action nonNull } } -#if __ANDROID__ - /// - /// Applies the framework element constraints like the size and max size, using an already measured view. - /// - /// - public static void OnMeasureOverride(T view) - where T : View, IFrameworkElement - { - var updated = IFrameworkElementHelper - .SizeThatFits(view, new _Size(view.MeasuredWidth, view.MeasuredHeight).PhysicalToLogicalPixels()) - .LogicalToPhysicalPixels(); - - Microsoft.UI.Xaml.Controls.Layouter.SetMeasuredDimensions(view, (int)updated.Width, (int)updated.Height); - } - - /// - /// Applies the framework element constraints like the size and max size, using the provided measured size. - /// - /// - public static void OnMeasureOverride(T view, _Size measuredSize) - where T : View, IFrameworkElement - { - var updated = IFrameworkElementHelper - .SizeThatFits(view, new _Size(measuredSize.Width, measuredSize.Height).PhysicalToLogicalPixels()) - .LogicalToPhysicalPixels(); - - Microsoft.UI.Xaml.Controls.Layouter.SetMeasuredDimensions(view, (int)updated.Width, (int)updated.Height); - } -#endif - /// /// Base constraint reasoning for simple containers that always respect the stretch of their children. /// diff --git a/src/Uno.UI/UI/Xaml/IFrameworkElementImplementation.Android.tt b/src/Uno.UI/UI/Xaml/IFrameworkElementImplementation.Android.tt index 21e35db05b86..938820928f02 100644 --- a/src/Uno.UI/UI/Xaml/IFrameworkElementImplementation.Android.tt +++ b/src/Uno.UI/UI/Xaml/IFrameworkElementImplementation.Android.tt @@ -42,10 +42,9 @@ namespace <#= mixin.NamespaceName #> { partial class <#= mixin.ClassName #> : IFrameworkElement, IXUidProvider, IFrameworkElementInternal { - private readonly static IEventProvider _trace = Tracing.Get(FrameworkElement.TraceProvider.Id); - #if !<#= mixin.IsFrameworkElement #> // IsFrameworkElement public event DependencyPropertyChangedEventHandler IsEnabledChanged; + private readonly static IEventProvider _trace = Tracing.Get(FrameworkElement.TraceProvider.Id); #endif public event TypedEventHandler Loading; diff --git a/src/Uno.UI/UI/Xaml/ILayouterElement.Android.cs b/src/Uno.UI/UI/Xaml/ILayouterElement.Android.cs deleted file mode 100644 index c14b2952473a..000000000000 --- a/src/Uno.UI/UI/Xaml/ILayouterElement.Android.cs +++ /dev/null @@ -1,59 +0,0 @@ -#nullable enable -using System; -using System.Runtime.CompilerServices; -using Uno.Extensions; -using Uno.UI; -using Windows.Foundation; - -namespace Microsoft.UI.Xaml; - -internal partial interface ILayouterElement -{ - bool StretchAffectsMeasure { get; } - - HorizontalAlignment HorizontalAlignment { get; } - - VerticalAlignment VerticalAlignment { get; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Size OnMeasureInternal(int widthMeasureSpec, int heightMeasureSpec) - { - try - { - var availableSize = ViewHelper.LogicalSizeFromSpec(widthMeasureSpec, heightMeasureSpec); - - this.DoMeasure(availableSize, out var measuredSizeLogical); - - var measuredSize = measuredSizeLogical.LogicalToPhysicalPixels(); - - if (StretchAffectsMeasure) - { - if (HorizontalAlignment == HorizontalAlignment.Stretch && - !double.IsPositiveInfinity(availableSize.Width)) - { - measuredSize.Width = ViewHelper.MeasureSpecGetSize(widthMeasureSpec); - } - - if (VerticalAlignment == VerticalAlignment.Stretch && !double.IsPositiveInfinity(availableSize.Height)) - { - measuredSize.Height = ViewHelper.MeasureSpecGetSize(heightMeasureSpec); - } - } - - // Report our final dimensions. - SetMeasuredDimensionInternal( - (int)measuredSize.Width, - (int)measuredSize.Height - ); - - return measuredSize; - } - catch (Exception ex) - { - Application.Current.RaiseRecoverableUnhandledExceptionOrLog(ex, this); - return default; - } - } - - internal void SetMeasuredDimensionInternal(int width, int height); -} diff --git a/src/Uno.UI/UI/Xaml/ILayouterElement.cs b/src/Uno.UI/UI/Xaml/ILayouterElement.cs index 9d43be9e9e72..7bcac0b54faa 100644 --- a/src/Uno.UI/UI/Xaml/ILayouterElement.cs +++ b/src/Uno.UI/UI/Xaml/ILayouterElement.cs @@ -13,130 +13,9 @@ namespace Microsoft.UI.Xaml; internal partial interface ILayouterElement { - internal ILayouter Layouter { get; } + internal Size Measure(Size availableSize); - internal Size LastAvailableSize { get; } - - internal bool IsMeasureDirty { get; } - - internal bool IsFirstMeasureDoneAndManagedElement { get; } - - internal bool IsMeasureDirtyPathDisabled { get; } + internal void Arrange(Rect finalRect); } -internal static class LayouterElementExtensions -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool DoMeasure(this ILayouterElement element, Size availableSize, out Size measuredSizeLogical) - { - var isFirstMeasure = !element.IsFirstMeasureDoneAndManagedElement; - - // "isDirty" here means this element's MeasureOverride - // method NEEDS to be called. - var isDirty = - isFirstMeasure // first time here since attached to parent - || (availableSize != element.LastAvailableSize) // size changed - || element.IsMeasureDirty // .InvalidateMeasure() called - || !FeatureConfiguration.UIElement.UseInvalidateMeasurePath // dirty_path disabled globally - || element.IsMeasureDirtyPathDisabled; // dirty_path disabled locally - - var frameworkElement = element as FrameworkElement; - if (frameworkElement is null) // native unmanaged element? - { - isDirty = true; - } - else if (!isDirty) - { - if (!frameworkElement.IsMeasureDirtyPath) - { - // That's a weird case, but we need to return something meaningful. - measuredSizeLogical = frameworkElement.DesiredSize; - return false; - } - if (element.GetParent() is not UIElement and not null) - { - // If the parent if this element is not managed (UIElement), - // .MeasureOverride() needs to be called. - isDirty = true; - } - } - - if (isFirstMeasure) - { - frameworkElement?.SetLayoutFlags(UIElement.LayoutFlag.FirstMeasureDone); - } - - var remainingTries = UIElement.MaxLayoutIterations; - measuredSizeLogical = default; - - while (--remainingTries > 0) - { - if (isDirty || frameworkElement is null) - { - // We must reset the flag **BEFORE** doing the actual measure, so the elements are able to re-invalidate themselves - frameworkElement?.ClearLayoutFlags(UIElement.LayoutFlag.MeasureDirty); - - // The dirty flag is explicitly set on this element - try - { - measuredSizeLogical = element.Layouter.Measure(availableSize); - } - catch (Exception e) - { - Application.Current.RaiseRecoverableUnhandledExceptionOrLog(e, element); - return false; - } - finally - { - LayoutInformation.SetAvailableSize(element, availableSize); - } - - return true; // end of isDirty processing - } - - // The measure dirty flag is set on one of the descendents: - // it will bypass the current element's .MeasureOverride() - // since it shouldn't produce a different result and it's - // just a waste of precious CPU time to call it. - using var children = frameworkElement.GetChildren().GetEnumerator(); - - while (children.MoveNext()) - { - var child = children.Current; - // If the child is dirty (or is a path to a dirty descendant child), - // We're remeasuring it. - - if (child is UIElement { IsMeasureDirtyOrMeasureDirtyPath: true } childAsUIElement) - { - var previousDesiredSize = childAsUIElement.m_desiredSize; - childAsUIElement.EnsureLayoutStorage(); - element.Layouter.MeasureChild(child, childAsUIElement.m_previousAvailableSize); - var newDesiredSize = childAsUIElement.m_desiredSize; - if (newDesiredSize != previousDesiredSize) - { - isDirty = true; - break; - } - } - else if (child is not UIElement) - { - isDirty = true; - break; - } - } - - if (!isDirty) - { - measuredSizeLogical = LayoutInformation.GetDesiredSize(element); - return true; // end of DIRTY_PATH processing - } - - // When the end of the loop is reached here, it means the - // DIRTY_PATH process has been _upgraded_ to a standard _isDirty - // process instead. - } - - return false; // UIElement.MaxLayoutIterations reached. Maybe an exception should be raised instead. - } -} #endif diff --git a/src/Uno.UI/UI/Xaml/ILayouterElement.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/ILayouterElement.iOSmacOS.cs deleted file mode 100644 index 1eede3403417..000000000000 --- a/src/Uno.UI/UI/Xaml/ILayouterElement.iOSmacOS.cs +++ /dev/null @@ -1,32 +0,0 @@ -#nullable enable -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Windows.Foundation; -using CoreGraphics; -using Uno.UI; - -namespace Microsoft.UI.Xaml; - -internal partial interface ILayouterElement -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool XamlMeasureInternal( - Size availableSize, - Size? lastAvailableSize, - out CGSize measuredSize) - { - if (IsMeasureDirty || availableSize != lastAvailableSize) - { - if (this.DoMeasure(availableSize, out var measuredSizeLogical)) - { - measuredSize = measuredSizeLogical.LogicalToPhysicalPixels(); - return true; - } - } - - // No need to measure - measuredSize = default; - return false; - } -} diff --git a/src/Uno.UI/UI/Xaml/Internal/RootVisual.cs b/src/Uno.UI/UI/Xaml/Internal/RootVisual.cs index 81653ac7b71c..2f0e455fc18e 100644 --- a/src/Uno.UI/UI/Xaml/Internal/RootVisual.cs +++ b/src/Uno.UI/UI/Xaml/Internal/RootVisual.cs @@ -104,4 +104,26 @@ protected override Size ArrangeOverride(Size finalSize) return finalSize; } + +#if __ANDROID__ + protected override void OnLayoutCore(bool changed, int left, int top, int right, int bottom, bool localIsLayoutRequested) + { + base.OnLayoutCore(changed, left, top, right, bottom, localIsLayoutRequested); + if (XamlRoot?.HostWindow?.Content is { } content) + { + content.UpdateLayout(); + } + } +#elif __IOS__ + + public override void LayoutIfNeeded() + { + base.LayoutIfNeeded(); + + if (XamlRoot?.HostWindow?.Content is { } content) + { + content.UpdateLayout(); + } + } +#endif } diff --git a/src/Uno.UI/UI/Xaml/LayoutStorage.cs b/src/Uno.UI/UI/Xaml/LayoutStorage.cs index 9a413e5d42c7..a2c600e411ce 100644 --- a/src/Uno.UI/UI/Xaml/LayoutStorage.cs +++ b/src/Uno.UI/UI/Xaml/LayoutStorage.cs @@ -22,10 +22,9 @@ partial class UIElement internal Size m_desiredSize; internal Rect m_finalRect; //internal Point m_offset; -#if UNO_REFERENCE_API - // On mobile, stored in Layouter + internal Size m_unclippedDesiredSize; -#endif + internal Size m_size; // Stores the layout clip, which is always an axis-aligned rect. diff --git a/src/Uno.UI/UI/Xaml/Shapes/Shape.Android.cs b/src/Uno.UI/UI/Xaml/Shapes/Shape.Android.cs index 80cd0fb2b8cc..a039aab170fe 100644 --- a/src/Uno.UI/UI/Xaml/Shapes/Shape.Android.cs +++ b/src/Uno.UI/UI/Xaml/Shapes/Shape.Android.cs @@ -37,17 +37,17 @@ protected override void OnDraw(Canvas canvas) } //Drawing paths on the canvas does not respect the canvas' ClipBounds - if (ClippedFrame is { } clippedFrame) - { - clippedFrame = clippedFrame.LogicalToPhysicalPixels(); - if (FrameRoundingAdjustment is { } fra) - { - clippedFrame.Width += fra.Width; - clippedFrame.Height += fra.Height; - } - - canvas.ClipRect(clippedFrame.ToRectF()); - } + //if (ClippedFrame is { } clippedFrame) + //{ + // clippedFrame = clippedFrame.LogicalToPhysicalPixels(); + // if (FrameRoundingAdjustment is { } fra) + // { + // clippedFrame.Width += fra.Width; + // clippedFrame.Height += fra.Height; + // } + + // canvas.ClipRect(clippedFrame.ToRectF()); + //} DrawFill(canvas); DrawStroke(canvas); diff --git a/src/Uno.UI/UI/Xaml/UIElement.Android.cs b/src/Uno.UI/UI/Xaml/UIElement.Android.cs index 581d576f0faf..e639ac3a0122 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Android.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Android.cs @@ -106,36 +106,6 @@ public UIElement() InitializePointers(); } - /// - /// On Android, the equivalent of the "Dirty Path" is the native - /// "Layout Requested" mechanism. - /// - internal bool IsMeasureDirtyPath - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutRequested; - } - - /// - /// Determines if InvalidateArrange has been called - /// - internal bool IsArrangeDirty => IsLayoutRequested; - - /// - /// Not implemented yet on this platform. - /// - internal bool IsArrangeDirtyPath - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => false; - } - - /// - /// Gets the **logical** frame (a.k.a. 'finalRect') of the element while it's being arranged by a managed parent. - /// - /// Used to keep "double" precision of arrange phase. - private protected Rect? TransientArrangeFinalRect { get; private set; } - /// /// The difference between the physical layout width and height taking the origin into account, /// and the physical width and height that would've been calculated for an origin of (0,0). @@ -148,22 +118,6 @@ internal bool IsArrangeDirtyPath /// internal Size? FrameRoundingAdjustment { get; set; } - internal void SetFramePriorArrange(Rect frame /* a.k.a 'finalRect' */, Rect physicalFrame) - { - var physicalWidth = ViewHelper.LogicalToPhysicalPixels(frame.Width); - var physicalHeight = ViewHelper.LogicalToPhysicalPixels(frame.Height); - - TransientArrangeFinalRect = frame; - FrameRoundingAdjustment = new Size( - (int)physicalFrame.Width - physicalWidth, - (int)physicalFrame.Height - physicalHeight); - } - - internal void ResetFramePostArrange() - { - TransientArrangeFinalRect = null; - } - partial void ApplyNativeClip(Rect rect) { if (rect.IsEmpty) @@ -203,6 +157,18 @@ partial void ApplyNativeClip(Rect rect) } } + internal void ArrangeVisual(Rect finalRect, Rect? clippedFrame = default) + { + // TODO: clipped frame? + var physical = finalRect.LogicalToPhysicalPixels(); + this.Layout( + (int)physical.Left, + (int)physical.Top, + (int)physical.Right, + (int)physical.Bottom + ); + } + /// /// This method is called from the OnDraw of elements supporting rounded corners: /// Border, Rectangle, Panel... diff --git a/src/Uno.UI/UI/Xaml/UIElement.Layout.Flags.cs b/src/Uno.UI/UI/Xaml/UIElement.Layout.Flags.cs new file mode 100644 index 000000000000..856062c3ac86 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/UIElement.Layout.Flags.cs @@ -0,0 +1,207 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.UI.Xaml +{ + partial class UIElement + { + /// + /// Determines if InvalidateMeasure has been called + /// + internal bool IsMeasureDirty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.MeasureDirty); + } + + internal bool IsMeasureDirtyPath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.MeasureDirtyPath); + } + + internal bool IsMeasureDirtyOrMeasureDirtyPath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsAnyLayoutFlagsSet(LayoutFlag.MeasureDirty | LayoutFlag.MeasureDirtyPath); + } + + /// + /// If the first measure has been done since the control + /// is connected to its parent + /// + internal bool IsFirstMeasureDone + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.FirstMeasureDone); + } + + /// + /// Determines if InvalidateArrange has been called + /// + internal bool IsArrangeDirty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.ArrangeDirty); + } + + internal bool IsArrangeDirtyPath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsAnyLayoutFlagsSet(LayoutFlag.ArrangeDirtyPath); + } + + internal bool IsArrangeDirtyOrArrangeDirtyPath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsAnyLayoutFlagsSet(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); + } + + /// + /// If the first arrange has been done since the control + /// is connected to its parent + /// + internal bool IsFirstArrangeDone + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.FirstArrangeDone); + } + + [Flags] + internal enum LayoutFlag : short + { + /// + /// Means the Measure is dirty for the current element + /// + MeasureDirty = 0b0000_0001, + + /// + /// Means the Measure is dirty on at least one child of this element + /// + MeasureDirtyPath = 0b0000_0010, + + /// + /// Indicates that the element is currently being measured during the Arrange cycle. + /// + MeasureDuringArrange = 0b0000_0001_0000_0000, + + /// + /// Indicates that the element is currently being measured. + /// + MeasuringSelf = 0b0000_0010_0000_0000, + + /// + /// Indicates that first measure has been done on the element after been connected to parent + /// + FirstMeasureDone = 0b0000_0100, + + /// + /// Means the MeasureDirtyPath is disabled on this element. + /// + /// + /// This flag is copied to children when they are attached, but can be re-enabled afterwards. + /// This flag is used during invalidation + /// + MeasureDirtyPathDisabled = 0b0000_1000, + + /// + /// Means the Arrange is dirty on the current element or one of its child + /// + ArrangeDirty = 0b0001_0000, + + /// + /// Means the Arrange is dirty on at least one child of this element + /// + ArrangeDirtyPath = 0b0010_0000, + + /// + /// Indicates that first arrange has been done on the element and we can use the + /// LayoutInformation.GetLayoutSlot() to get previous finalRect + /// + FirstArrangeDone = 0b0100_0000, + + /// + /// Means the MeasureDirtyPath is disabled on this element. + /// + /// + /// This flag is copied to children when they are attached, but can be re-enabled afterwards. + /// This flag is used during invalidation + /// + ArrangeDirtyPathDisabled = 0b1000_0000, + } + + private const LayoutFlag DEFAULT_STARTING_LAYOUTFLAGS = 0; + private const LayoutFlag LAYOUT_FLAGS_TO_CLEAR_ON_RESET = + LayoutFlag.MeasureDirty | + LayoutFlag.MeasureDirtyPath | + LayoutFlag.MeasureDuringArrange | + LayoutFlag.MeasuringSelf | + LayoutFlag.ArrangeDirty | + LayoutFlag.ArrangeDirtyPath | + LayoutFlag.FirstArrangeDone | + LayoutFlag.FirstMeasureDone; + + private LayoutFlag _layoutFlags = DEFAULT_STARTING_LAYOUTFLAGS; + + /// + /// Check for one specific layout flag + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool IsLayoutFlagSet(LayoutFlag flag) => (_layoutFlags & flag) == flag; + + /// + /// Check that at least one of the specified flags is set + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool IsAnyLayoutFlagsSet(LayoutFlag flags) => (_layoutFlags & flags) != 0; + + /// + /// Set one or many flags (set to 1) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetLayoutFlags(LayoutFlag flags) => _layoutFlags |= flags; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetLayoutFlags(LayoutFlag flags, bool state) + { + if (state) + { + SetLayoutFlags(flags); + } + else + { + ClearLayoutFlags(flags); + } + } + + /// + /// Reset one or many flags (set flag to zero) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ClearLayoutFlags(LayoutFlag flags) => _layoutFlags &= ~flags; + + /// + /// Reset flags to original state + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ResetLayoutFlags() => ClearLayoutFlags(LAYOUT_FLAGS_TO_CLEAR_ON_RESET); + + internal bool IsMeasureDirtyPathDisabled + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.MeasureDirtyPathDisabled); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => SetLayoutFlags(LayoutFlag.MeasureDirtyPathDisabled, value); + } + + internal bool IsArrangeDirtyPathDisabled + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.ArrangeDirtyPathDisabled); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => SetLayoutFlags(LayoutFlag.ArrangeDirtyPathDisabled, value); + } + } +} diff --git a/src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs b/src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs deleted file mode 100644 index b45f26c83a6c..000000000000 --- a/src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs +++ /dev/null @@ -1,498 +0,0 @@ -#if !__NETSTD_REFERENCE__ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Uno.Foundation.Logging; -using Uno.UI; -using Uno.UI.Extensions; -using Uno.UI.Xaml; -using Windows.Foundation; - -namespace Microsoft.UI.Xaml -{ - public partial class UIElement : DependencyObject - { - /// - /// When set, measure and invalidate requests will not be propagated further up the visual tree, ie they won't trigger a re-layout. - /// Used where repeated unnecessary measure/arrange passes would be unacceptable for performance (eg scrolling in a list). - /// - internal bool ShouldInterceptInvalidate { get; set; } - - public void InvalidateMeasure() - { - if (ShouldInterceptInvalidate || IsMeasureDirty || IsLayoutFlagSet(LayoutFlag.MeasuringSelf)) - { - return; - } - - if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) - { - this.Log().LogWarning($"[LayoutCycleTracing] InvalidateMeasure {this},{this.GetDebugName()}"); - } - - SetLayoutFlags(LayoutFlag.MeasureDirty); - - if (FeatureConfiguration.UIElement.UseInvalidateMeasurePath && !IsMeasureDirtyPathDisabled) - { - InvalidateParentMeasureDirtyPath(); - } - else - { - (this.GetParent() as UIElement)?.InvalidateMeasure(); - if (IsVisualTreeRoot) - { - XamlRoot.InvalidateMeasure(); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InvalidateMeasureDirtyPath() - { - if (IsMeasureDirtyOrMeasureDirtyPath) - { - return; // Already invalidated - } - - SetLayoutFlags(LayoutFlag.MeasureDirtyPath); - - InvalidateParentMeasureDirtyPath(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void InvalidateParentMeasureDirtyPath() - { - if (this.GetParent() is UIElement parent) //TODO: Should this use VisualTree.GetParent as fallback? https://github.com/unoplatform/uno/issues/8978 - { - parent.InvalidateMeasureDirtyPath(); - } - else if (IsVisualTreeRoot) - { - XamlRoot.InvalidateMeasure(); - } - } - - public void InvalidateArrange() - { - if (ShouldInterceptInvalidate) - { - return; - } - - if (IsArrangeDirty) - { - return; // Already dirty - } - - if (_traceLayoutCycle) - { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().LogError($"[LayoutCycleTracing] InvalidateArrange {this},{this.GetDebugName()}"); - } - - if (this.Log().IsEnabled(LogLevel.Trace)) - { - this.Log().Trace($"[LayoutCycleTracing] {Environment.StackTrace}"); - } - } - - SetLayoutFlags(LayoutFlag.ArrangeDirty); - - if (FeatureConfiguration.UIElement.UseInvalidateArrangePath && !IsArrangeDirtyPathDisabled) - { - InvalidateParentArrangeDirtyPath(); - } - else - { - (this.GetParent() as UIElement)?.InvalidateArrange(); - if (IsVisualTreeRoot) - { - XamlRoot.InvalidateArrange(); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InvalidateArrangeDirtyPath() - { - if (IsArrangeDirtyOrArrangeDirtyPath) - { - return; // Already invalidated - } - - SetLayoutFlags(LayoutFlag.ArrangeDirtyPath); - - InvalidateParentArrangeDirtyPath(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InvalidateParentArrangeDirtyPath() - { - if (this.GetParent() is UIElement parent) //TODO: Should this use VisualTree.GetParent as fallback? https://github.com/unoplatform/uno/issues/8978 - { - parent.InvalidateArrangeDirtyPath(); - } - else //TODO: Why not check IsVisualTreeRoot as in InvalidateParentMeasureDirtyPath? - { - XamlRoot?.InvalidateArrange(); - } - } - - public void Measure(Size availableSize) - { - if (!_isFrameworkElement) - { - return; // Only FrameworkElements are measurable - } - - // Visibility should not be checked here. Consider the following scenario: - // 1. A collapsed element is measured before it enters the visual tree - // 2. Visibility changes to Visible after it enters the visual tree, which will call InvalidateMeasure - // In this case, we want step 1 to clear the dirty flag. - // If the flag isn't cleared in step 1, then InvalidateMeasure call in step 2 will do nothing because the - // element is already dirty, which means the dirtiness isn't propagated up to RootVisual. - // So, we want to go into DoMeasure, which will clear the flag. - // Then, DoMeasure is going to early return if Visibility is collapsed. - - if (IsVisualTreeRoot) - { - MeasureVisualTreeRoot(availableSize); - } - else - { - // If possible we avoid the try/finally which might be costly on some platforms - DoMeasure(availableSize); - } - } - - /// - /// This method contains or is called by a try/catch containing method and - /// can be significantly slower than other methods as a result on WebAssembly. - /// See https://github.com/dotnet/runtime/issues/56309 - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void MeasureVisualTreeRoot(Size availableSize) - { - try - { - _isLayoutingVisualTreeRoot = true; - DoMeasure(availableSize); - } - finally - { - _isLayoutingVisualTreeRoot = false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoMeasure(Size availableSize) - { - var isFirstMeasure = !IsLayoutFlagSet(LayoutFlag.FirstMeasureDone); - - var isDirty = - isFirstMeasure - || (availableSize != m_previousAvailableSize) - || IsMeasureDirty - || !FeatureConfiguration.UIElement.UseInvalidateMeasurePath // dirty_path disabled globally - || IsMeasureDirtyPathDisabled; - - var isMeasureDirtyPath = IsMeasureDirtyPath; - - if (!isDirty && !isMeasureDirtyPath) - { - return; // Nothing to do - } - - if (isFirstMeasure) - { - SetLayoutFlags(LayoutFlag.FirstMeasureDone); - } - - var remainingTries = MaxLayoutIterations; - - while (--remainingTries > 0) - { - if (isDirty) - { - // We must reset the flag **BEFORE** doing the actual measure, so the elements are able to re-invalidate themselves - // TODO: This doesn't actually follow WinUI. It looks like in WinUI, the method - // CUIElement::MeasureInternal is doing SetIsMeasureDirty(FALSE); at the end. - // If we were able to align this to WinUI, we should remember clearing - // the flag in the Visibility == Visibility.Collapsed case as well. - // The Visibility condition is similar to the GetIsLayoutSuspended check in - // WinUI, which does goto Cleanup and will call SetIsMeasureDirty(FALSE); - ClearLayoutFlags(LayoutFlag.MeasureDirty | LayoutFlag.MeasureDirtyPath); - - var prevSize = DesiredSize; - - // The dirty flag is explicitly set on this element -#if DEBUG - try -#endif - { - if (this.Visibility == Visibility.Collapsed) - { - m_desiredSize = default; - RecursivelyApplyTemplateWorkaround(); - return; - } - - SetLayoutFlags(LayoutFlag.MeasuringSelf); - MeasureCore(availableSize); - ClearLayoutFlags(LayoutFlag.MeasuringSelf); - InvalidateArrange(); - } -#if DEBUG - catch (Exception ex) - { - _log.Error($"Error measuring {this}", ex); - throw; - } - finally -#endif - { - m_previousAvailableSize = availableSize; - - // if (!GetIsMeasureDuringArrange() && ! IsSameSize(prevSize, desiredSize) && !bInLayoutTransition) - if (!IsLayoutFlagSet(LayoutFlag.MeasureDuringArrange) && prevSize != DesiredSize) - { - if (GetUIElementAdjustedParentInternal() is { } pParent) - { - pParent.OnChildDesiredSizeChanged(pParent); - } - } - } - - break; - } - - // isMeasureDirtyPath is always true here - ClearLayoutFlags(LayoutFlag.MeasureDirtyPath); - - // The dirty flag is set on one of the descendents: - // it will bypass the current element's MeasureOverride() - // since it shouldn't produce a different result and it's - // just a waste of precious CPU time to call it. - var children = GetChildren().GetEnumerator(); - - //foreach (var child in children) - while (children.MoveNext()) - { - if (children.Current is { IsMeasureDirtyOrMeasureDirtyPath: true } child) - { - // If the child is dirty (or is a path to a dirty descendant child), - // We're remeasuring it. - - var previousDesiredSize = child.DesiredSize; - child.EnsureLayoutStorage(); - child.Measure(child.m_previousAvailableSize); - if (child.DesiredSize != previousDesiredSize) - { - isDirty = true; - break; - } - } - } - - children.Dispose(); // no "using" operator here to prevent an implicit try-catch on Wasm - - if (isDirty) - { - continue; - } - - break; - } - } - - internal virtual void MeasureCore(Size availableSize) - { - throw new NotSupportedException("UIElement doesn't implement MeasureCore. Inherit from FrameworkElement, which properly implements MeasureCore."); - } - - internal bool ShouldApplyLayoutClipAsAncestorClip() - { - return this is Panel; // Restrict to Panels, to limit app-compat risk - //&& !GetIsScrollViewerHeader(); // Special-case: ScrollViewer Headers, which can zoom, must scale the LayoutClip too - } - - private void RecursivelyApplyTemplateWorkaround() - { - // Uno workaround. The template should NOT be applied here. - // But, without this workaround, VerifyVisibilityChangeUpdatesCommandBarVisualState test will fail. - // The real root cause for the test failure is that FindParentCommandBarForElement will - // return null, that is because Uno doesn't yet properly have a "logical parent" concept. - // We eagerly apply the template so that FindParentCommandBarForElement will - // find the command bar through TemplatedParent - if (this is Control thisAsControl) - { - thisAsControl.TryCallOnApplyTemplate(); - - // Update bindings to ensure resources defined - // in visual parents get applied. - this.UpdateResourceBindings(); - } - - foreach (var child in _children) - { - child.RecursivelyApplyTemplateWorkaround(); - } - } - - - public void Arrange(Rect finalRect) - { - if (!_isFrameworkElement) - { - return; - } - - var firstArrangeDone = IsFirstArrangeDone; - - if (Visibility == Visibility.Collapsed) - { - m_finalRect = finalRect; - HideVisual(); - ClearLayoutFlags(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); - return; - } - - if (firstArrangeDone && !IsArrangeDirtyOrArrangeDirtyPath && finalRect == m_finalRect) - { - ClearLayoutFlags(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); - return; // Calling Arrange would be a waste of CPU time here. - } - - if (IsVisualTreeRoot) - { - ArrangeVisualTreeRoot(finalRect); - } - else - { - // If possible we avoid the try/finally which might be costly on some platforms - DoArrange(finalRect); - } - } - - /// - /// This method contains or is called by a try/catch containing method and can be significantly slower than other methods as a result on WebAssembly. - /// See https://github.com/dotnet/runtime/issues/56309 - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ArrangeVisualTreeRoot(Rect finalRect) - { - try - { - _isLayoutingVisualTreeRoot = true; - DoArrange(finalRect); - } - finally - { - _isLayoutingVisualTreeRoot = false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoArrange(Rect finalRect) - { - var isFirstArrange = !IsLayoutFlagSet(LayoutFlag.FirstArrangeDone); - - var isDirty = - isFirstArrange - || IsArrangeDirty - || finalRect != m_finalRect; - - if (!isDirty && !IsArrangeDirtyPath) - { - return; // Nothing do to - } - - if (GetUseLayoutRounding()) - { - finalRect.X = LayoutRound(finalRect.X); - finalRect.Y = LayoutRound(finalRect.Y); - finalRect.Width = LayoutRound(finalRect.Width); - finalRect.Height = LayoutRound(finalRect.Height); - } - - var remainingTries = MaxLayoutIterations; - - while (--remainingTries > 0) - { - if (IsMeasureDirtyOrMeasureDirtyPath) - { - // Uno doc: in WinUI, the flag is only set and reset if IsMeasureDirty, not IsMeasureDirtyOrMeasureDirtyPath - SetLayoutFlags(LayoutFlag.MeasureDuringArrange); - DoMeasure(m_previousAvailableSize); - ClearLayoutFlags(LayoutFlag.MeasureDuringArrange); - } - - if (isDirty) - { - ShowVisual(); - - // We must store the updated slot before natively arranging the element, - // so the updated value can be read by indirect code that is being invoked on arrange. - // For instance, the EffectiveViewPort computation reads that value to detect slot changes (cf. PropagateEffectiveViewportChange) - m_finalRect = finalRect; - - // We must reset the flag **BEFORE** doing the actual arrange, so the elements are able to re-invalidate themselves - ClearLayoutFlags(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); - - ArrangeCore(finalRect); - - SetLayoutFlags(LayoutFlag.FirstArrangeDone); - - break; - } - else if (IsArrangeDirtyPath) - { - ClearLayoutFlags(LayoutFlag.ArrangeDirtyPath); - - var children = GetChildren().GetEnumerator(); - - while (children.MoveNext()) - { - var child = children.Current; - - if (child is { IsArrangeDirtyOrArrangeDirtyPath: true }) - { - var previousRenderSize = child.RenderSize; - child.Arrange(child.m_finalRect); - - if (child.RenderSize != previousRenderSize) - { - isDirty = true; - break; - } - } - } - - children.Dispose(); // no "using" operator here to prevent an implicit try-catch on Wasm - - if (!isDirty) - { - break; - } - } - else - { - break; - } - } - - } - - partial void HideVisual(); - partial void ShowVisual(); - - internal virtual void ArrangeCore(Rect finalRect) - { - throw new NotSupportedException("UIElement doesn't implement ArrangeCore. Inherit from FrameworkElement, which properly implements ArrangeCore."); - } - } -} -#endif diff --git a/src/Uno.UI/UI/Xaml/UIElement.Layout.cs b/src/Uno.UI/UI/Xaml/UIElement.Layout.cs index 63465f100732..aec9b4b9409f 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Layout.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Layout.cs @@ -1,255 +1,534 @@ -#if __WASM__ || __SKIA__ -// "Managed Measure Dirty Path" means it's the responsibility of the -// managed code (Uno) to walk the tree and do the measure phase. -#define IMPLEMENTS_MANAGED_MEASURE_DIRTY_PATH -#define IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH -#endif +#if !__NETSTD_REFERENCE__ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Uno.Foundation.Logging; +using Uno.UI; +using Uno.UI.Extensions; +using Uno.UI.Xaml; +using Windows.Foundation; namespace Microsoft.UI.Xaml { - partial class UIElement + public partial class UIElement : DependencyObject { /// - /// Determines if InvalidateMeasure has been called + /// When set, measure and invalidate requests will not be propagated further up the visual tree, ie they won't trigger a re-layout. + /// Used where repeated unnecessary measure/arrange passes would be unacceptable for performance (eg scrolling in a list). /// - internal bool IsMeasureDirty + internal bool ShouldInterceptInvalidate { get; set; } + + public void InvalidateMeasure() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutFlagSet(LayoutFlag.MeasureDirty); + if (ShouldInterceptInvalidate || IsMeasureDirty || IsLayoutFlagSet(LayoutFlag.MeasuringSelf)) + { + return; + } + + if (_traceLayoutCycle && this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().LogWarning($"[LayoutCycleTracing] InvalidateMeasure {this},{this.GetDebugName()}"); + } + + SetLayoutFlags(LayoutFlag.MeasureDirty); + + if (FeatureConfiguration.UIElement.UseInvalidateMeasurePath && !IsMeasureDirtyPathDisabled) + { + InvalidateParentMeasureDirtyPath(); + } + else + { + (this.GetParent() as UIElement)?.InvalidateMeasure(); + if (IsVisualTreeRoot) + { +#if __CROSSRUNTIME__ + XamlRoot.InvalidateMeasure(); +#endif + } + } } -#if IMPLEMENTS_MANAGED_MEASURE_DIRTY_PATH - internal bool IsMeasureDirtyPath + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InvalidateMeasureDirtyPath() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutFlagSet(LayoutFlag.MeasureDirtyPath); + if (IsMeasureDirtyOrMeasureDirtyPath) + { + return; // Already invalidated + } + + SetLayoutFlags(LayoutFlag.MeasureDirtyPath); + + InvalidateParentMeasureDirtyPath(); } -#else - // IsMeasureDirtyPath implemented in platform-specific file to - // connect to native mechanisms. -#endif -#if IMPLEMENTS_MANAGED_MEASURE_DIRTY_PATH - internal bool IsMeasureDirtyOrMeasureDirtyPath + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void InvalidateParentMeasureDirtyPath() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsAnyLayoutFlagsSet(LayoutFlag.MeasureDirty | LayoutFlag.MeasureDirtyPath); + if (this.GetParent() is UIElement parent) //TODO: Should this use VisualTree.GetParent as fallback? https://github.com/unoplatform/uno/issues/8978 + { + parent.InvalidateMeasureDirtyPath(); + } + else if (IsVisualTreeRoot) + { +#if __CROSSRUNTIME__ + XamlRoot.InvalidateMeasure(); +#endif + } } -#else - /// - /// This is for compatibility - not implemented yet on this platform - /// - internal bool IsMeasureDirtyOrMeasureDirtyPath + + public void InvalidateArrange() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsMeasureDirty || IsMeasureDirtyPath; - } + if (ShouldInterceptInvalidate) + { + return; + } + + if (IsArrangeDirty) + { + return; // Already dirty + } + + if (_traceLayoutCycle) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().LogError($"[LayoutCycleTracing] InvalidateArrange {this},{this.GetDebugName()}"); + } + + if (this.Log().IsEnabled(LogLevel.Trace)) + { + this.Log().Trace($"[LayoutCycleTracing] {Environment.StackTrace}"); + } + } + + SetLayoutFlags(LayoutFlag.ArrangeDirty); + + if (FeatureConfiguration.UIElement.UseInvalidateArrangePath && !IsArrangeDirtyPathDisabled) + { + InvalidateParentArrangeDirtyPath(); + } + else + { + (this.GetParent() as UIElement)?.InvalidateArrange(); + if (IsVisualTreeRoot) + { +#if __CROSSRUNTIME__ + XamlRoot.InvalidateArrange(); +#elif __ANDROID__ + this.RequestLayout(); +#elif __IOS__ + this.SetNeedsLayout(); +#elif __MACOS__ + // TODO #endif + } + } + } - /// - /// If the first measure has been done since the control - /// is connected to its parent - /// - internal bool IsFirstMeasureDone + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InvalidateArrangeDirtyPath() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutFlagSet(LayoutFlag.FirstMeasureDone); + if (IsArrangeDirtyOrArrangeDirtyPath) + { + return; // Already invalidated + } + + SetLayoutFlags(LayoutFlag.ArrangeDirtyPath); + + InvalidateParentArrangeDirtyPath(); } -#if !__ANDROID__ - /// - /// Determines if InvalidateArrange has been called - /// - internal bool IsArrangeDirty + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InvalidateParentArrangeDirtyPath() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutFlagSet(LayoutFlag.ArrangeDirty); - } + if (this.GetParent() is UIElement parent) //TODO: Should this use VisualTree.GetParent as fallback? https://github.com/unoplatform/uno/issues/8978 + { + parent.InvalidateArrangeDirtyPath(); + } + else //TODO: Why not check IsVisualTreeRoot as in InvalidateParentMeasureDirtyPath? + { +#if __CROSSRUNTIME__ + XamlRoot?.InvalidateArrange(); +#elif __ANDROID__ + RequestLayout(); #endif + } + } -#if IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH - internal bool IsArrangeDirtyPath + public void Measure(Size availableSize) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsAnyLayoutFlagsSet(LayoutFlag.ArrangeDirtyPath); +#if __CROSSRUNTIME__ + if (!_isFrameworkElement) +#else + if (this is not FrameworkElement) +#endif + { + return; // Only FrameworkElements are measurable + } + + // Visibility should not be checked here. Consider the following scenario: + // 1. A collapsed element is measured before it enters the visual tree + // 2. Visibility changes to Visible after it enters the visual tree, which will call InvalidateMeasure + // In this case, we want step 1 to clear the dirty flag. + // If the flag isn't cleared in step 1, then InvalidateMeasure call in step 2 will do nothing because the + // element is already dirty, which means the dirtiness isn't propagated up to RootVisual. + // So, we want to go into DoMeasure, which will clear the flag. + // Then, DoMeasure is going to early return if Visibility is collapsed. + + if (IsVisualTreeRoot) + { + MeasureVisualTreeRoot(availableSize); + } + else + { + // If possible we avoid the try/finally which might be costly on some platforms + DoMeasure(availableSize); + } } - internal bool IsArrangeDirtyOrArrangeDirtyPath + /// + /// This method contains or is called by a try/catch containing method and + /// can be significantly slower than other methods as a result on WebAssembly. + /// See https://github.com/dotnet/runtime/issues/56309 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MeasureVisualTreeRoot(Size availableSize) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsAnyLayoutFlagsSet(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); + try + { + _isLayoutingVisualTreeRoot = true; + DoMeasure(availableSize); + } + finally + { + _isLayoutingVisualTreeRoot = false; + } } - /// - /// If the first arrange has been done since the control - /// is connected to its parent - /// - internal bool IsFirstArrangeDone + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoMeasure(Size availableSize) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutFlagSet(LayoutFlag.FirstArrangeDone); - } + var isFirstMeasure = !IsLayoutFlagSet(LayoutFlag.FirstMeasureDone); + + var isDirty = + isFirstMeasure + || (availableSize != m_previousAvailableSize) + || IsMeasureDirty + || !FeatureConfiguration.UIElement.UseInvalidateMeasurePath // dirty_path disabled globally + || IsMeasureDirtyPathDisabled; + + var isMeasureDirtyPath = IsMeasureDirtyPath; + if (!isDirty && !isMeasureDirtyPath) + { + return; // Nothing to do + } + + if (isFirstMeasure) + { + SetLayoutFlags(LayoutFlag.FirstMeasureDone); + } + + var remainingTries = MaxLayoutIterations; + + while (--remainingTries > 0) + { + if (isDirty) + { + // We must reset the flag **BEFORE** doing the actual measure, so the elements are able to re-invalidate themselves + // TODO: This doesn't actually follow WinUI. It looks like in WinUI, the method + // CUIElement::MeasureInternal is doing SetIsMeasureDirty(FALSE); at the end. + // If we were able to align this to WinUI, we should remember clearing + // the flag in the Visibility == Visibility.Collapsed case as well. + // The Visibility condition is similar to the GetIsLayoutSuspended check in + // WinUI, which does goto Cleanup and will call SetIsMeasureDirty(FALSE); + ClearLayoutFlags(LayoutFlag.MeasureDirty | LayoutFlag.MeasureDirtyPath); + + var prevSize = DesiredSize; + + // The dirty flag is explicitly set on this element +#if DEBUG + try +#endif + { + if (this.Visibility == Visibility.Collapsed) + { + m_desiredSize = default; + RecursivelyApplyTemplateWorkaround(); + return; + } + + SetLayoutFlags(LayoutFlag.MeasuringSelf); + MeasureCore(availableSize); + ClearLayoutFlags(LayoutFlag.MeasuringSelf); + InvalidateArrange(); + } +#if DEBUG + catch (Exception ex) + { +#if __CROSSRUNTIME__ + _log.Error($"Error measuring {this}", ex); #else - /// - /// This is for compatibility - not implemented yet on this platform - /// - internal bool IsArrangeDirtyOrArrangeDirtyPath + _ = ex; // TODO +#endif + throw; + } + finally +#endif + { + m_previousAvailableSize = availableSize; + + // if (!GetIsMeasureDuringArrange() && ! IsSameSize(prevSize, desiredSize) && !bInLayoutTransition) + if (!IsLayoutFlagSet(LayoutFlag.MeasureDuringArrange) && prevSize != DesiredSize) + { + if (GetUIElementAdjustedParentInternal() is { } pParent) + { + pParent.OnChildDesiredSizeChanged(pParent); + } + } + } + + break; + } + + // isMeasureDirtyPath is always true here + ClearLayoutFlags(LayoutFlag.MeasureDirtyPath); + + // The dirty flag is set on one of the descendents: + // it will bypass the current element's MeasureOverride() + // since it shouldn't produce a different result and it's + // just a waste of precious CPU time to call it. + var children = this.GetChildren().GetEnumerator(); + + //foreach (var child in children) + while (children.MoveNext()) + { + if (children.Current is UIElement { IsMeasureDirtyOrMeasureDirtyPath: true } child) + { + // If the child is dirty (or is a path to a dirty descendant child), + // We're remeasuring it. + + var previousDesiredSize = child.DesiredSize; + child.EnsureLayoutStorage(); + child.Measure(child.m_previousAvailableSize); + if (child.DesiredSize != previousDesiredSize) + { + isDirty = true; + break; + } + } + } + + children.Dispose(); // no "using" operator here to prevent an implicit try-catch on Wasm + + if (isDirty) + { + continue; + } + + break; + } + } + + internal virtual void MeasureCore(Size availableSize) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsArrangeDirty || IsArrangeDirtyPath; + throw new NotSupportedException("UIElement doesn't implement MeasureCore. Inherit from FrameworkElement, which properly implements MeasureCore."); } -#endif - [Flags] - internal enum LayoutFlag : short - { - /// - /// Means the Measure is dirty for the current element - /// - MeasureDirty = 0b0000_0001, - -#if IMPLEMENTS_MANAGED_MEASURE_DIRTY_PATH - /// - /// Means the Measure is dirty on at least one child of this element - /// - MeasureDirtyPath = 0b0000_0010, - - /// - /// Indicates that the element is currently being measured during the Arrange cycle. - /// - MeasureDuringArrange = 0b0000_0001_0000_0000, - - /// - /// Indicates that the element is currently being measured. - /// - MeasuringSelf = 0b0000_0010_0000_0000, -#endif + internal bool ShouldApplyLayoutClipAsAncestorClip() + { + return this is Panel; // Restrict to Panels, to limit app-compat risk + //&& !GetIsScrollViewerHeader(); // Special-case: ScrollViewer Headers, which can zoom, must scale the LayoutClip too + } - /// - /// Indicates that first measure has been done on the element after been connected to parent - /// - FirstMeasureDone = 0b0000_0100, - - /// - /// Means the MeasureDirtyPath is disabled on this element. - /// - /// - /// This flag is copied to children when they are attached, but can be re-enabled afterwards. - /// This flag is used during invalidation - /// - MeasureDirtyPathDisabled = 0b0000_1000, - -#if !__ANDROID__ // On Android, it's directly connected to IsLayoutRequested - /// - /// Means the Arrange is dirty on the current element or one of its child - /// - ArrangeDirty = 0b0001_0000, -#endif + private void RecursivelyApplyTemplateWorkaround() + { + // Uno workaround. The template should NOT be applied here. + // But, without this workaround, VerifyVisibilityChangeUpdatesCommandBarVisualState test will fail. + // The real root cause for the test failure is that FindParentCommandBarForElement will + // return null, that is because Uno doesn't yet properly have a "logical parent" concept. + // We eagerly apply the template so that FindParentCommandBarForElement will + // find the command bar through TemplatedParent + if (this is Control thisAsControl) + { + thisAsControl.TryCallOnApplyTemplate(); -#if IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH - /// - /// Means the Arrange is dirty on at least one child of this element - /// - ArrangeDirtyPath = 0b0010_0000, - - /// - /// Indicates that first arrange has been done on the element and we can use the - /// LayoutInformation.GetLayoutSlot() to get previous finalRect - /// - FirstArrangeDone = 0b0100_0000, -#endif + // Update bindings to ensure resources defined + // in visual parents get applied. + this.UpdateResourceBindings(); + } - /// - /// Means the MeasureDirtyPath is disabled on this element. - /// - /// - /// This flag is copied to children when they are attached, but can be re-enabled afterwards. - /// This flag is used during invalidation - /// - ArrangeDirtyPathDisabled = 0b1000_0000, - } - - private const LayoutFlag DEFAULT_STARTING_LAYOUTFLAGS = 0; - private const LayoutFlag LAYOUT_FLAGS_TO_CLEAR_ON_RESET = - LayoutFlag.MeasureDirty | -#if IMPLEMENTS_MANAGED_MEASURE_DIRTY_PATH - LayoutFlag.MeasureDirtyPath | - LayoutFlag.MeasureDuringArrange | - LayoutFlag.MeasuringSelf | +#if __CROSSRUNTIME__ + foreach (var child in _children) +#else + foreach (var childView in this.GetChildren()) #endif -#if !__ANDROID__ - LayoutFlag.ArrangeDirty | + { +#if !__CROSSRUNTIME__ + if (childView is UIElement child) #endif -#if IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH - LayoutFlag.ArrangeDirtyPath | - LayoutFlag.FirstArrangeDone | + { + child.RecursivelyApplyTemplateWorkaround(); + } + } + } + + public void Arrange(Rect finalRect) + { +#if __CROSSRUNTIME__ + if (!_isFrameworkElement) +#else + if (this is not FrameworkElement) #endif - LayoutFlag.FirstMeasureDone; + { + return; + } - private LayoutFlag _layoutFlags = DEFAULT_STARTING_LAYOUTFLAGS; + var firstArrangeDone = IsFirstArrangeDone; - /// - /// Check for one specific layout flag - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool IsLayoutFlagSet(LayoutFlag flag) => (_layoutFlags & flag) == flag; + if (Visibility == Visibility.Collapsed) + { + m_finalRect = finalRect; + HideVisual(); + ClearLayoutFlags(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); + return; + } - /// - /// Check that at least one of the specified flags is set - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool IsAnyLayoutFlagsSet(LayoutFlag flags) => (_layoutFlags & flags) != 0; + if (firstArrangeDone && !IsArrangeDirtyOrArrangeDirtyPath && finalRect == m_finalRect) + { + ClearLayoutFlags(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); + return; // Calling Arrange would be a waste of CPU time here. + } - /// - /// Set one or many flags (set to 1) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SetLayoutFlags(LayoutFlag flags) => _layoutFlags |= flags; + if (IsVisualTreeRoot) + { + ArrangeVisualTreeRoot(finalRect); + } + else + { + // If possible we avoid the try/finally which might be costly on some platforms + DoArrange(finalRect); + } + } + /// + /// This method contains or is called by a try/catch containing method and can be significantly slower than other methods as a result on WebAssembly. + /// See https://github.com/dotnet/runtime/issues/56309 + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SetLayoutFlags(LayoutFlag flags, bool state) + private void ArrangeVisualTreeRoot(Rect finalRect) { - if (state) + try { - SetLayoutFlags(flags); + _isLayoutingVisualTreeRoot = true; + DoArrange(finalRect); } - else + finally { - ClearLayoutFlags(flags); + _isLayoutingVisualTreeRoot = false; } } - /// - /// Reset one or many flags (set flag to zero) - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void ClearLayoutFlags(LayoutFlag flags) => _layoutFlags &= ~flags; + private void DoArrange(Rect finalRect) + { + var isFirstArrange = !IsLayoutFlagSet(LayoutFlag.FirstArrangeDone); - /// - /// Reset flags to original state - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void ResetLayoutFlags() => ClearLayoutFlags(LAYOUT_FLAGS_TO_CLEAR_ON_RESET); + var isDirty = + isFirstArrange + || IsArrangeDirty + || finalRect != m_finalRect; - internal bool IsMeasureDirtyPathDisabled - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutFlagSet(LayoutFlag.MeasureDirtyPathDisabled); + if (!isDirty && !IsArrangeDirtyPath) + { + return; // Nothing do to + } + + if (GetUseLayoutRounding()) + { + finalRect.X = LayoutRound(finalRect.X); + finalRect.Y = LayoutRound(finalRect.Y); + finalRect.Width = LayoutRound(finalRect.Width); + finalRect.Height = LayoutRound(finalRect.Height); + } + + var remainingTries = MaxLayoutIterations; + + while (--remainingTries > 0) + { + if (IsMeasureDirtyOrMeasureDirtyPath) + { + // Uno doc: in WinUI, the flag is only set and reset if IsMeasureDirty, not IsMeasureDirtyOrMeasureDirtyPath + SetLayoutFlags(LayoutFlag.MeasureDuringArrange); + DoMeasure(m_previousAvailableSize); + ClearLayoutFlags(LayoutFlag.MeasureDuringArrange); + } + + if (isDirty) + { + ShowVisual(); + + // We must store the updated slot before natively arranging the element, + // so the updated value can be read by indirect code that is being invoked on arrange. + // For instance, the EffectiveViewPort computation reads that value to detect slot changes (cf. PropagateEffectiveViewportChange) + m_finalRect = finalRect; + + // We must reset the flag **BEFORE** doing the actual arrange, so the elements are able to re-invalidate themselves + ClearLayoutFlags(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); + + ArrangeCore(finalRect); + + SetLayoutFlags(LayoutFlag.FirstArrangeDone); + + break; + } + else if (IsArrangeDirtyPath) + { + ClearLayoutFlags(LayoutFlag.ArrangeDirtyPath); + + var children = this.GetChildren().GetEnumerator(); + + while (children.MoveNext()) + { + var child = children.Current; + + if (child is UIElement { IsArrangeDirtyOrArrangeDirtyPath: true } childAsUIElement) + { + var previousRenderSize = childAsUIElement.RenderSize; + childAsUIElement.Arrange(childAsUIElement.m_finalRect); + + if (childAsUIElement.RenderSize != previousRenderSize) + { + isDirty = true; + break; + } + } + } + + children.Dispose(); // no "using" operator here to prevent an implicit try-catch on Wasm + + if (!isDirty) + { + break; + } + } + else + { + break; + } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => SetLayoutFlags(LayoutFlag.MeasureDirtyPathDisabled, value); } - internal bool IsArrangeDirtyPathDisabled - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => IsLayoutFlagSet(LayoutFlag.ArrangeDirtyPathDisabled); + partial void HideVisual(); + partial void ShowVisual(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => SetLayoutFlags(LayoutFlag.ArrangeDirtyPathDisabled, value); + internal virtual void ArrangeCore(Rect finalRect) + { + throw new NotSupportedException("UIElement doesn't implement ArrangeCore. Inherit from FrameworkElement, which properly implements ArrangeCore."); } } } +#endif diff --git a/src/Uno.UI/UI/Xaml/UIElement.cs b/src/Uno.UI/UI/Xaml/UIElement.cs index aeb9fcab2327..3dd7de9e18a8 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.cs @@ -801,10 +801,12 @@ private static void InnerUpdateLayout(UIElement root) #elif __ANDROID__ for (var i = 0; i < MaxLayoutIterations; i++) { - // On Android, Measure and arrange are the same if (root.IsMeasureDirtyOrMeasureDirtyPath) { root.Measure(bounds.Size); + } + else if (root.IsArrangeDirtyOrArrangeDirtyPath) + { root.Arrange(bounds); } else @@ -933,35 +935,12 @@ internal void ApplyClip() #elif __WASM__ InvalidateArrange(); -#else - Rect rect; - - if (Clip == null) - { - rect = Rect.Empty; - - if (NeedsClipToSlot) - { -#if UNO_REFERENCE_API - rect = new Rect(0, 0, RenderSize.Width, RenderSize.Height); -#else - rect = ClippedFrame ?? Rect.Empty; -#endif - } - } - else - { - rect = Clip.Rect; - - // Apply transform to clipping mask, if any - if (Clip.Transform != null) - { - rect = Clip.Transform.TransformBounds(rect); - } - } - - ApplyNativeClip(rect); - OnViewportUpdated(rect); +#elif __ANDROID__ + // TODO: +#elif __IOS__ + // TODO: +#elif __MACOS__ + // TODO: #endif } @@ -1078,102 +1057,6 @@ public Size RenderSize } #endif - -#if !UNO_REFERENCE_API - - /// - /// This is the Frame that should be used as "available Size" for the Arrange phase. - /// - internal Rect? ClippedFrame; - - /// - /// Updates the DesiredSize of a UIElement. Typically, objects that implement custom layout for their - /// layout children call this method from their own MeasureOverride implementations to form a recursive layout update. - /// - /// - /// The available space that a parent can allocate to a child object. A child object can request a larger - /// space than what is available; the provided size might be accommodated if scrolling or other resize behavior is - /// possible in that particular container. - /// - /// The measured size. - /// - /// Under Uno.UI, this method should not be called during the normal layouting phase. Instead, use the - /// methods, which handles native view properly. - /// - public void Measure(Size availableSize) - { - EnsureLayoutStorage(); - - if (this is not FrameworkElement fwe) - { - return; - } - - if (double.IsNaN(availableSize.Width) || double.IsNaN(availableSize.Height)) - { - throw new InvalidOperationException($"Cannot measure [{GetType()}] with NaN"); - } - - ((ILayouterElement)fwe).Layouter.Measure(availableSize); -#if IS_UNIT_TESTS - OnMeasurePartial(availableSize); -#endif - } - -#if IS_UNIT_TESTS - partial void OnMeasurePartial(Size slotSize); -#endif - - /// - /// Positions child objects and determines a size for a UIElement. Parent objects that implement custom layout - /// for their child elements should call this method from their layout override implementations to form a recursive layout update. - /// - /// The final size that the parent computes for the child in layout, provided as a value. - public void Arrange(Rect finalRect) - { - EnsureLayoutStorage(); - - if (this is not FrameworkElement fwe) - { - return; - } - - var layouter = ((ILayouterElement)fwe).Layouter; - layouter.Arrange(finalRect.DeflateBy(fwe.Margin)); - layouter.ArrangeChild(fwe, finalRect); - } - - public void InvalidateMeasure() - { -#if __ANDROID__ - // Use a non-virtual version of the RequestLayout method, for performance. - base.RequestLayout(); - SetLayoutFlags(LayoutFlag.MeasureDirty); -#elif __IOS__ - SetNeedsLayout(); - SetLayoutFlags(LayoutFlag.MeasureDirty); -#elif __MACOS__ - base.NeedsLayout = true; - SetLayoutFlags(LayoutFlag.MeasureDirty); -#endif - - OnInvalidateMeasure(); - } - - protected internal virtual void OnInvalidateMeasure() - { - } - - [global::Uno.NotImplemented] - public void InvalidateArrange() - { - InvalidateMeasure(); -#if __IOS__ || __MACOS__ - SetLayoutFlags(LayoutFlag.ArrangeDirty); -#endif - } -#endif - /// /// This method has to be invoked for elements that are going to be recycled WITHOUT necessarily being unloaded / loaded. /// For instance, this is not expected to be invoked for elements recycled by the template pool as they are always unloaded. diff --git a/src/Uno.UI/UI/Xaml/UIElement.iOS.cs b/src/Uno.UI/UI/Xaml/UIElement.iOS.cs index 2377133ccfad..d2a34705270d 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.iOS.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.iOS.cs @@ -47,18 +47,6 @@ public override void MovedToWindow() } } - internal bool IsMeasureDirtyPath - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => false; // Not implemented on iOS yet - } - - internal bool IsArrangeDirtyPath - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => false; // Not implemented on iOS yet - } - internal bool ClippingIsSetByCornerRadius { get; set; } partial void ApplyNativeClip(Rect rect) @@ -153,6 +141,12 @@ public void SetSubviewsNeedLayout() } } + internal void ArrangeVisual(Rect finalRect, Rect? clippedFrame = default) + { + // TODO: clipped frame? + this.Frame = ViewHelper.LogicalToPhysicalPixels(finalRect); + } + internal global::Windows.Foundation.Point GetPosition(Point position, global::Microsoft.UI.Xaml.UIElement relativeTo) { return ConvertPointToCoordinateSpace(position, relativeTo); diff --git a/src/Uno.UI/UI/Xaml/UIElement.macOS.cs b/src/Uno.UI/UI/Xaml/UIElement.macOS.cs index 3638103e7f4b..f0d4932a15d5 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.macOS.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.macOS.cs @@ -26,18 +26,6 @@ public UIElement() UpdateHitTest(); } - internal bool IsMeasureDirtyPath - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => false; // Not implemented on macOS yet - } - - internal bool IsArrangeDirtyPath - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => false; // Not implemented on macOS yet - } - internal bool ClippingIsSetByCornerRadius { get; set; } partial void OnOpacityChanged(DependencyPropertyChangedEventArgs args) diff --git a/src/Uno.UI/UI/Xaml/UIElement.reference.cs b/src/Uno.UI/UI/Xaml/UIElement.reference.cs index a7b45afae1f7..c22ee7cb6343 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.reference.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.reference.cs @@ -18,10 +18,6 @@ public UIElement() public IntPtr Handle { get; } - internal bool IsMeasureDirtyPath => throw new NotSupportedException("Reference assembly"); - - internal bool IsArrangeDirtyPath => throw new NotSupportedException("Reference assembly"); - internal bool ShouldInterceptInvalidate { get; set; } internal void AddChild(UIElement child, int? index = null) => throw new NotSupportedException("Reference assembly"); diff --git a/src/Uno.UI/UI/Xaml/UIElement.unittests.cs b/src/Uno.UI/UI/Xaml/UIElement.unittests.cs index 3b3dac6fc4b0..5671eced4375 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.unittests.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.unittests.cs @@ -22,10 +22,6 @@ public UIElement() public string Name { get; set; } - internal bool IsMeasureDirtyPath => false; - - internal bool IsArrangeDirtyPath => false; - public int MeasureCallCount { get; protected set; } public int ArrangeCallCount { get; protected set; }