using System; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Reactive; using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; #nullable enable namespace Avalonia.Layout { /// /// Defines how a control aligns itself horizontally in its parent control. /// public enum HorizontalAlignment { /// /// The control stretches to fill the width of the parent control. /// Stretch, /// /// The control aligns itself to the left of the parent control. /// Left, /// /// The control centers itself in the parent control. /// Center, /// /// The control aligns itself to the right of the parent control. /// Right, } /// /// Defines how a control aligns itself vertically in its parent control. /// public enum VerticalAlignment { /// /// The control stretches to fill the height of the parent control. /// Stretch, /// /// The control aligns itself to the top of the parent control. /// Top, /// /// The control centers itself within the parent control. /// Center, /// /// The control aligns itself to the bottom of the parent control. /// Bottom, } /// /// Implements layout-related functionality for a control. /// public class Layoutable : Visual { /// /// Defines the property. /// public static readonly DirectProperty DesiredSizeProperty = AvaloniaProperty.RegisterDirect(nameof(DesiredSize), o => o.DesiredSize); /// /// Defines the property. /// public static readonly StyledProperty WidthProperty = AvaloniaProperty.Register(nameof(Width), double.NaN, validate: ValidateDimension); /// /// Defines the property. /// public static readonly StyledProperty HeightProperty = AvaloniaProperty.Register(nameof(Height), double.NaN, validate: ValidateDimension); /// /// Defines the property. /// public static readonly StyledProperty MinWidthProperty = AvaloniaProperty.Register(nameof(MinWidth), validate: ValidateMinimumDimension); /// /// Defines the property. /// public static readonly StyledProperty MaxWidthProperty = AvaloniaProperty.Register(nameof(MaxWidth), double.PositiveInfinity, validate: ValidateMaximumDimension); /// /// Defines the property. /// public static readonly StyledProperty MinHeightProperty = AvaloniaProperty.Register(nameof(MinHeight), validate: ValidateMinimumDimension); /// /// Defines the property. /// public static readonly StyledProperty MaxHeightProperty = AvaloniaProperty.Register(nameof(MaxHeight), double.PositiveInfinity, validate: ValidateMaximumDimension); /// /// Defines the property. /// public static readonly StyledProperty MarginProperty = AvaloniaProperty.Register(nameof(Margin)); /// /// Defines the property. /// public static readonly StyledProperty HorizontalAlignmentProperty = AvaloniaProperty.Register(nameof(HorizontalAlignment)); /// /// Defines the property. /// public static readonly StyledProperty VerticalAlignmentProperty = AvaloniaProperty.Register(nameof(VerticalAlignment)); /// /// Defines the property. /// public static readonly StyledProperty UseLayoutRoundingProperty = AvaloniaProperty.Register(nameof(UseLayoutRounding), defaultValue: true, inherits: true); private bool _measuring; private Size? _previousMeasure; private Rect? _previousArrange; private EventHandler? _effectiveViewportChanged; private EventHandler? _layoutUpdated; private bool _isAttachingToVisualTree; /// /// Initializes static members of the class. /// static Layoutable() { AffectsMeasure( WidthProperty, HeightProperty, MinWidthProperty, MaxWidthProperty, MinHeightProperty, MaxHeightProperty, MarginProperty, HorizontalAlignmentProperty, VerticalAlignmentProperty); } private static bool ValidateDimension(double value) => double.IsNaN(value) || ValidateMinimumDimension(value); private static bool ValidateMinimumDimension(double value) => !double.IsPositiveInfinity(value) && ValidateMaximumDimension(value); private static bool ValidateMaximumDimension(double value) => value >= 0; /// /// Occurs when the element's effective viewport changes. /// public event EventHandler? EffectiveViewportChanged { add { if (_effectiveViewportChanged is null && this.GetLayoutRoot() is {} r && !_isAttachingToVisualTree) { r.LayoutManager.RegisterEffectiveViewportListener(this); } _effectiveViewportChanged += value; } remove { _effectiveViewportChanged -= value; if (_effectiveViewportChanged is null && this.GetLayoutRoot() is {} r) { r.LayoutManager.UnregisterEffectiveViewportListener(this); } } } /// /// Occurs when a layout pass completes for the control. /// public event EventHandler? LayoutUpdated { add { if (_layoutUpdated is null && this.GetLayoutRoot() is {} r && !_isAttachingToVisualTree) { r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; } _layoutUpdated += value; } remove { _layoutUpdated -= value; if (_layoutUpdated is null && this.GetLayoutRoot() is {} r) { r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; } } } /// /// Executes a layout pass. /// /// /// You should not usually need to call this method explictly, the layout manager will /// schedule layout passes itself. /// public void UpdateLayout() => this.GetLayoutManager()?.ExecuteLayoutPass(); /// /// Gets or sets the width of the element. /// public double Width { get { return GetValue(WidthProperty); } set { SetValue(WidthProperty, value); } } /// /// Gets or sets the height of the element. /// public double Height { get { return GetValue(HeightProperty); } set { SetValue(HeightProperty, value); } } /// /// Gets or sets the minimum width of the element. /// public double MinWidth { get { return GetValue(MinWidthProperty); } set { SetValue(MinWidthProperty, value); } } /// /// Gets or sets the maximum width of the element. /// public double MaxWidth { get { return GetValue(MaxWidthProperty); } set { SetValue(MaxWidthProperty, value); } } /// /// Gets or sets the minimum height of the element. /// public double MinHeight { get { return GetValue(MinHeightProperty); } set { SetValue(MinHeightProperty, value); } } /// /// Gets or sets the maximum height of the element. /// public double MaxHeight { get { return GetValue(MaxHeightProperty); } set { SetValue(MaxHeightProperty, value); } } /// /// Gets or sets the margin around the element. /// public Thickness Margin { get { return GetValue(MarginProperty); } set { SetValue(MarginProperty, value); } } /// /// Gets or sets the element's preferred horizontal alignment in its parent. /// public HorizontalAlignment HorizontalAlignment { get { return GetValue(HorizontalAlignmentProperty); } set { SetValue(HorizontalAlignmentProperty, value); } } /// /// Gets or sets the element's preferred vertical alignment in its parent. /// public VerticalAlignment VerticalAlignment { get { return GetValue(VerticalAlignmentProperty); } set { SetValue(VerticalAlignmentProperty, value); } } /// /// Gets the size that this element computed during the measure pass of the layout process. /// public Size DesiredSize { get; private set; } /// /// Gets a value indicating whether the control's layout measure is valid. /// public bool IsMeasureValid { get; private set; } /// /// Gets a value indicating whether the control's layouts arrange is valid. /// public bool IsArrangeValid { get; private set; } /// /// Gets or sets a value that determines whether the element should be snapped to pixel /// boundaries at layout time. /// public bool UseLayoutRounding { get { return GetValue(UseLayoutRoundingProperty); } set { SetValue(UseLayoutRoundingProperty, value); } } /// /// Gets the available size passed in the previous layout pass, if any. /// internal Size? PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// internal Rect? PreviousArrange => _previousArrange; /// /// Creates the visual children of the control, if necessary /// public virtual void ApplyTemplate() { } /// /// Carries out a measure of the control. /// /// The available size for the control. public void Measure(Size availableSize) { if (double.IsNaN(availableSize.Width) || double.IsNaN(availableSize.Height)) { throw new InvalidOperationException("Cannot call Measure using a size with NaN values."); } if (!IsMeasureValid || _previousMeasure != availableSize) { using var activity = Diagnostic.MeasuringLayoutable()? .AddTag(Diagnostic.Tags.Control, this); var previousDesiredSize = DesiredSize; var desiredSize = default(Size); IsMeasureValid = true; try { _measuring = true; desiredSize = MeasureCore(availableSize); } finally { _measuring = false; } if (IsInvalidSize(desiredSize)) { throw new InvalidOperationException("Invalid size returned for Measure."); } DesiredSize = desiredSize; _previousMeasure = availableSize; Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Measure requested {DesiredSize}", DesiredSize); if (DesiredSize != previousDesiredSize) { this.GetVisualParent()?.ChildDesiredSizeChanged(this); } } } /// /// Arranges the control and its children. /// /// The control's new bounds. public void Arrange(Rect rect) { if (IsInvalidRect(rect)) { throw new InvalidOperationException("Invalid Arrange rectangle."); } if (!IsMeasureValid) { Measure(_previousMeasure ?? rect.Size); } if (!IsArrangeValid || _previousArrange != rect) { using var activity = Diagnostic.ArrangingLayoutable()? .AddTag(Diagnostic.Tags.Control, this); Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect); IsArrangeValid = true; ArrangeCore(rect); _previousArrange = rect; } } /// /// Invalidates the measurement of the control and queues a new layout pass. /// public void InvalidateMeasure() { if (IsMeasureValid) { Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated measure"); IsMeasureValid = false; IsArrangeValid = false; if (IsAttachedToVisualTree) { this.GetLayoutManager()?.InvalidateMeasure(this); InvalidateVisual(); } OnMeasureInvalidated(); } } /// /// Invalidates the arrangement of the control and queues a new layout pass. /// public void InvalidateArrange() { if (IsArrangeValid) { Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated arrange"); IsArrangeValid = false; this.GetLayoutManager()?.InvalidateArrange(this); InvalidateVisual(); } } /// /// Called when a child control's desired size changes. /// /// The child control. internal void ChildDesiredSizeChanged(Layoutable control) { if (!_measuring) { InvalidateMeasure(); } } internal void RaiseEffectiveViewportChanged(EffectiveViewportChangedEventArgs e) { _effectiveViewportChanged?.Invoke(this, e); } /// /// Marks a property as affecting the control's measurement. /// /// The control which the property affects. /// The properties. /// /// After a call to this method in a control's static constructor, any change to the /// property will cause to be called on the element. /// protected static void AffectsMeasure(params AvaloniaProperty[] properties) where T : Layoutable { var invalidateObserver = new AnonymousObserver( static e => (e.Sender as T)?.InvalidateMeasure()); foreach (var property in properties) { property.Changed.Subscribe(invalidateObserver); } } /// /// Marks a property as affecting the control's arrangement. /// /// The control which the property affects. /// The properties. /// /// After a call to this method in a control's static constructor, any change to the /// property will cause to be called on the element. /// protected static void AffectsArrange(params AvaloniaProperty[] properties) where T : Layoutable { var invalidate = new AnonymousObserver( static e => (e.Sender as T)?.InvalidateArrange()); foreach (var property in properties) { property.Changed.Subscribe(invalidate); } } /// /// The default implementation of the control's measure pass. /// /// The size available to the control. /// The desired size for the control. /// /// This method calls which is probably the method you /// want to override in order to modify a control's arrangement. /// protected virtual Size MeasureCore(Size availableSize) { if (IsVisible) { var margin = Margin; var useLayoutRounding = UseLayoutRounding; var scale = 1.0; if (useLayoutRounding) { scale = LayoutHelper.GetLayoutScale(this); margin = LayoutHelper.RoundLayoutThickness(margin, scale); } ApplyStyling(); ApplyTemplate(); var minMax = new MinMax(this); var constrainedSize = LayoutHelper.ApplyLayoutConstraints( minMax, availableSize.Deflate(margin)); var isContainer = false; ContainerSizing containerSizing = ContainerSizing.Normal; if (Container.GetQueryProvider(this) is { } queryProvider && Container.GetSizing(this) is { } sizing && sizing != ContainerSizing.Normal) { isContainer = true; containerSizing = sizing; queryProvider.SetSize(constrainedSize.Width, constrainedSize.Height, containerSizing); } var measured = MeasureOverride(constrainedSize); var width = MathUtilities.Clamp(measured.Width, minMax.MinWidth, minMax.MaxWidth); var height = MathUtilities.Clamp(measured.Height, minMax.MinHeight, minMax.MaxHeight); if (isContainer) { switch (containerSizing) { case ContainerSizing.Width: width = double.IsInfinity(constrainedSize.Width) ? width : constrainedSize.Width; break; case ContainerSizing.Height: width = measured.Width; height = double.IsInfinity(constrainedSize.Height) ? height : constrainedSize.Height; break; case ContainerSizing.WidthAndHeight: width = double.IsInfinity(constrainedSize.Width) ? width : constrainedSize.Width; height = double.IsInfinity(constrainedSize.Height) ? height : constrainedSize.Height; break; } } if (useLayoutRounding) { (width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale); } width += margin.Left + margin.Right; height += margin.Top + margin.Bottom; if (width > availableSize.Width) width = availableSize.Width; if (height > availableSize.Height) height = availableSize.Height; if (width < 0) width = 0; if (height < 0) height = 0; return new Size(width, height); } else { return new Size(); } } /// /// Measures the control and its child elements as part of a layout pass. /// /// The size available to the control. /// The desired size for the control. protected virtual Size MeasureOverride(Size availableSize) { double width = 0; double height = 0; var visualChildren = VisualChildren; var visualCount = visualChildren.Count; for (var i = 0; i < visualCount; i++) { Visual visual = visualChildren[i]; if (visual is Layoutable layoutable) { layoutable.Measure(availableSize); var childSize = layoutable.DesiredSize; if (childSize.Width > width) width = childSize.Width; if (childSize.Height > height) height = childSize.Height; } } return new Size(width, height); } /// /// The default implementation of the control's arrange pass. /// /// The control's new bounds. /// /// This method calls which is probably the method you /// want to override in order to modify a control's arrangement. /// protected virtual void ArrangeCore(Rect finalRect) { if (IsVisible) { var useLayoutRounding = UseLayoutRounding; var scale = LayoutHelper.GetLayoutScale(this); var margin = Margin; var originX = finalRect.X + margin.Left; var originY = finalRect.Y + margin.Top; // Margin has to be treated separately because the layout rounding function is not linear // f(a + b) != f(a) + f(b) // If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales. if (useLayoutRounding) { margin = LayoutHelper.RoundLayoutThickness(margin, scale); } var availableWidthMinusMargins = finalRect.Width - margin.Left - margin.Right; if (availableWidthMinusMargins < 0) availableWidthMinusMargins = 0; var availableHeightMinusMargins = finalRect.Height - margin.Top - margin.Bottom; if (availableHeightMinusMargins < 0) availableHeightMinusMargins = 0; var availableSizeMinusMargins = new Size(availableWidthMinusMargins, availableHeightMinusMargins); var horizontalAlignment = HorizontalAlignment; var verticalAlignment = VerticalAlignment; var size = availableSizeMinusMargins; if (horizontalAlignment != HorizontalAlignment.Stretch) { size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - margin.Left - margin.Right)); } if (verticalAlignment != VerticalAlignment.Stretch) { size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - margin.Top - margin.Bottom)); } size = LayoutHelper.ApplyLayoutConstraints(new MinMax(this), size); if (useLayoutRounding) { size = LayoutHelper.RoundLayoutSizeUp(size, scale); availableSizeMinusMargins = LayoutHelper.RoundLayoutSizeUp(availableSizeMinusMargins, scale); } size = ArrangeOverride(size).Constrain(size); switch (horizontalAlignment) { case HorizontalAlignment.Center: case HorizontalAlignment.Stretch: originX += (availableSizeMinusMargins.Width - size.Width) / 2; break; case HorizontalAlignment.Right: originX += availableSizeMinusMargins.Width - size.Width; break; } switch (verticalAlignment) { case VerticalAlignment.Center: case VerticalAlignment.Stretch: originY += (availableSizeMinusMargins.Height - size.Height) / 2; break; case VerticalAlignment.Bottom: originY += availableSizeMinusMargins.Height - size.Height; break; } var origin = new Point(originX, originY); if (useLayoutRounding) { origin = LayoutHelper.RoundLayoutPoint(origin, scale); } Bounds = new Rect(origin, size); } } /// /// Positions child elements as part of a layout pass. /// /// The size available to the control. /// The actual size used. protected virtual Size ArrangeOverride(Size finalSize) { var arrangeRect = new Rect(finalSize); var visualChildren = VisualChildren; var visualCount = visualChildren.Count; for (var i = 0; i < visualCount; i++) { Visual visual = visualChildren[i]; if (visual is Layoutable layoutable) { layoutable.Arrange(arrangeRect); } } return finalSize; } internal sealed override void InvalidateStyles(bool recurse) { base.InvalidateStyles(recurse); InvalidateMeasure(); } /// protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { _isAttachingToVisualTree = true; try { base.OnAttachedToVisualTreeCore(e); } finally { _isAttachingToVisualTree = false; } if (this.GetLayoutRoot() is {} r) { if (_layoutUpdated is object) { r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; } if (_effectiveViewportChanged is object) { r.LayoutManager.RegisterEffectiveViewportListener(this); } } } protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { if (this.GetLayoutRoot() is {} r) { if (_layoutUpdated is object) { r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; } if (_effectiveViewportChanged is object) { r.LayoutManager.UnregisterEffectiveViewportListener(this); } } base.OnDetachedFromVisualTreeCore(e); } /// /// Called by InvalidateMeasure /// protected virtual void OnMeasureInvalidated() { } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsVisibleProperty) { DesiredSize = default; // All changes to visibility cause the parent element to be notified. this.GetVisualParent()?.ChildDesiredSizeChanged(this); if (change.GetNewValue()) { // We only invalidate ourselves when visibility is changed to true. InvalidateMeasure(); // If any descendant had its measure/arrange invalidated while we were hidden, // they will need to be registered with the layout manager now that they // are again effectively visible. If IsEffectivelyVisible becomes an observable // property then we can piggy-pack on that; for the moment we do this manually. if (this.GetLayoutRoot() is {} layoutRoot) { var count = VisualChildren.Count; for (var i = 0; i < count; ++i) { (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutRoot.LayoutManager); } } } } } /// protected sealed override void OnVisualParentChanged(Visual? oldParent, Visual? newParent) { LayoutHelper.InvalidateSelfAndChildrenMeasure(this); base.OnVisualParentChanged(oldParent, newParent); } private protected override void OnControlThemeChanged() { base.OnControlThemeChanged(); InvalidateMeasure(); } internal override void OnTemplatedParentControlThemeChanged() { base.OnTemplatedParentControlThemeChanged(); InvalidateMeasure(); } private void AncestorBecameVisible(ILayoutManager layoutManager) { if (!IsVisible) return; if (!IsMeasureValid) { layoutManager.InvalidateMeasure(this); InvalidateVisual(); } else if (!IsArrangeValid) { layoutManager.InvalidateArrange(this); InvalidateVisual(); } var count = VisualChildren.Count; for (var i = 0; i < count; ++i) { (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutManager); } } /// /// Called when the layout manager raises a LayoutUpdated event. /// /// The sender. /// The event args. private void LayoutManagedLayoutUpdated(object? sender, EventArgs e) => _layoutUpdated?.Invoke(this, e); /// /// Tests whether any of a 's properties include negative values, /// a NaN or Infinity. /// /// The rect. /// True if the rect is invalid; otherwise false. private static bool IsInvalidRect(Rect rect) { return MathUtilities.IsNegativeOrNonFinite(rect.Width) || MathUtilities.IsNegativeOrNonFinite(rect.Height) || !MathUtilities.IsFinite(rect.X) || !MathUtilities.IsFinite(rect.Y); } /// /// Tests whether any of a 's properties include negative values, /// a NaN or Infinity. /// /// The size. /// True if the size is invalid; otherwise false. private static bool IsInvalidSize(Size size) { return MathUtilities.IsNegativeOrNonFinite(size.Width) || MathUtilities.IsNegativeOrNonFinite(size.Height); } /// /// Ensures neither component of a is negative. /// /// The size. /// The non-negative size. private static Size NonNegative(Size size) { return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0)); } } }