using System; using Avalonia.Logging; using Avalonia.Utilities; using Avalonia.VisualTree; 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, ILayoutable { /// /// 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); /// /// Defines the property. /// public static readonly StyledProperty HeightProperty = AvaloniaProperty.Register(nameof(Height), double.NaN); /// /// Defines the property. /// public static readonly StyledProperty MinWidthProperty = AvaloniaProperty.Register(nameof(MinWidth)); /// /// Defines the property. /// public static readonly StyledProperty MaxWidthProperty = AvaloniaProperty.Register(nameof(MaxWidth), double.PositiveInfinity); /// /// Defines the property. /// public static readonly StyledProperty MinHeightProperty = AvaloniaProperty.Register(nameof(MinHeight)); /// /// Defines the property. /// public static readonly StyledProperty MaxHeightProperty = AvaloniaProperty.Register(nameof(MaxHeight), double.PositiveInfinity); /// /// 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; /// /// Initializes static members of the class. /// static Layoutable() { AffectsMeasure( IsVisibleProperty, WidthProperty, HeightProperty, MinWidthProperty, MaxWidthProperty, MinHeightProperty, MaxHeightProperty, MarginProperty, HorizontalAlignmentProperty, VerticalAlignmentProperty); } /// /// Occurs when a layout pass completes for the control. /// public event EventHandler LayoutUpdated; /// /// 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. /// Size? ILayoutable.PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// Rect? ILayoutable.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) { 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) { Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect); IsArrangeValid = true; ArrangeCore(rect); _previousArrange = rect; LayoutUpdated?.Invoke(this, EventArgs.Empty); } } /// /// Called by InvalidateMeasure /// protected virtual void OnMeasureInvalidated() { } /// /// 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 (((ILayoutable)this).IsAttachedToVisualTree) { (VisualRoot as ILayoutRoot)?.LayoutManager.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; (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); InvalidateVisual(); } } /// void ILayoutable.ChildDesiredSizeChanged(ILayoutable control) { if (!_measuring) { InvalidateMeasure(); } } /// /// Marks a property as affecting the control's measurement. /// /// 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. /// [Obsolete("Use AffectsMeasure and specify the control type.")] protected static void AffectsMeasure(params AvaloniaProperty[] properties) { AffectsMeasure(properties); } /// /// 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 : class, ILayoutable { void Invalidate(AvaloniaPropertyChangedEventArgs e) { (e.Sender as T)?.InvalidateMeasure(); } foreach (var property in properties) { property.Changed.Subscribe(Invalidate); } } /// /// Marks a property as affecting the control's arrangement. /// /// 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. /// [Obsolete("Use AffectsArrange and specify the control type.")] protected static void AffectsArrange(params AvaloniaProperty[] properties) { AffectsArrange(properties); } /// /// 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 : class, ILayoutable { void Invalidate(AvaloniaPropertyChangedEventArgs 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; ApplyStyling(); ApplyTemplate(); var constrained = LayoutHelper.ApplyLayoutConstraints( this, availableSize.Deflate(margin)); var measured = MeasureOverride(constrained); var width = measured.Width; var height = measured.Height; { double widthCache = Width; if (!double.IsNaN(widthCache)) { width = widthCache; } } width = Math.Min(width, MaxWidth); width = Math.Max(width, MinWidth); { double heightCache = Height; if (!double.IsNaN(heightCache)) { height = heightCache; } } height = Math.Min(height, MaxHeight); height = Math.Max(height, MinHeight); width = Math.Min(width, availableSize.Width); height = Math.Min(height, availableSize.Height); if (UseLayoutRounding) { var scale = GetLayoutScale(); width = LayoutHelper.RoundLayoutValue(width, scale); height = LayoutHelper.RoundLayoutValue(height, scale); } return NonNegative(new Size(width, height).Inflate(margin)); } 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++) { IVisual visual = visualChildren[i]; if (visual is ILayoutable layoutable) { layoutable.Measure(availableSize); width = Math.Max(width, layoutable.DesiredSize.Width); height = Math.Max(height, layoutable.DesiredSize.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 margin = Margin; var originX = finalRect.X + margin.Left; var originY = finalRect.Y + margin.Top; var availableSizeMinusMargins = new Size( Math.Max(0, finalRect.Width - margin.Left - margin.Right), Math.Max(0, finalRect.Height - margin.Top - margin.Bottom)); var horizontalAlignment = HorizontalAlignment; var verticalAlignment = VerticalAlignment; var size = availableSizeMinusMargins; var scale = GetLayoutScale(); var useLayoutRounding = UseLayoutRounding; 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(this, size); if (useLayoutRounding) { size = LayoutHelper.RoundLayoutSize(size, scale, scale); availableSizeMinusMargins = LayoutHelper.RoundLayoutSize(availableSizeMinusMargins, scale, 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; } if (useLayoutRounding) { originX = LayoutHelper.RoundLayoutValue(originX, scale); originY = LayoutHelper.RoundLayoutValue(originY, scale); } Bounds = new Rect(originX, originY, size.Width, size.Height); } } /// /// 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++) { IVisual visual = visualChildren[i]; if (visual is ILayoutable layoutable) { layoutable.Arrange(arrangeRect); } } return finalSize; } protected sealed override void InvalidateStyles() { base.InvalidateStyles(); InvalidateMeasure(); } /// protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { LayoutHelper.InvalidateSelfAndChildrenMeasure(this); base.OnVisualParentChanged(oldParent, newParent); } /// /// 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 rect.Width < 0 || rect.Height < 0 || double.IsInfinity(rect.X) || double.IsInfinity(rect.Y) || double.IsInfinity(rect.Width) || double.IsInfinity(rect.Height) || double.IsNaN(rect.X) || double.IsNaN(rect.Y) || double.IsNaN(rect.Width) || double.IsNaN(rect.Height); } /// /// 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 size.Width < 0 || size.Height < 0 || double.IsInfinity(size.Width) || double.IsInfinity(size.Height) || double.IsNaN(size.Width) || double.IsNaN(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)); } private double GetLayoutScale() { var result = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; if (result == 0 || double.IsNaN(result) || double.IsInfinity(result)) { throw new Exception($"Invalid LayoutScaling returned from {VisualRoot.GetType()}"); } return result; } } }