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));
}
}
}