using System.ComponentModel; namespace Terminal.Gui; /// /// Aligns items within a container based on the specified . Both horizontal and vertical /// alignments are supported. /// public class Aligner : INotifyPropertyChanged { private Alignment _alignment; /// /// Gets or sets how the aligns items within a container. /// /// /// /// provides additional options for aligning items in a container. /// /// public Alignment Alignment { get => _alignment; set { _alignment = value; PropertyChanged?.Invoke (this, new (nameof (Alignment))); } } private AlignmentModes _alignmentMode = AlignmentModes.StartToEnd; /// /// Gets or sets the modes controlling . /// public AlignmentModes AlignmentModes { get => _alignmentMode; set { _alignmentMode = value; PropertyChanged?.Invoke (this, new (nameof (AlignmentModes))); } } private int _containerSize; /// /// The size of the container. /// public int ContainerSize { get => _containerSize; set { _containerSize = value; PropertyChanged?.Invoke (this, new (nameof (ContainerSize))); } } /// public event PropertyChangedEventHandler PropertyChanged; /// /// Takes a list of item sizes and returns a list of the positions of those items when aligned within /// /// using the and settings. /// /// The sizes of the items to align. /// The locations of the items, from left/top to right/bottom. public int [] Align (int [] sizes) { return Align (Alignment, AlignmentModes, ContainerSize, sizes); } /// /// Takes a list of item sizes and returns a list of the positions of those items when aligned within /// /// using specified parameters. /// /// Specifies how the items will be aligned. /// /// The size of the container. /// The sizes of the items to align. /// The positions of the items, from left/top to right/bottom. public static int [] Align (in Alignment alignment, in AlignmentModes alignmentMode, in int containerSize, in int [] sizes) { if (sizes.Length == 0) { return []; } var sizesCopy = sizes; if (alignmentMode.FastHasFlags (AlignmentModes.EndToStart)) { sizesCopy = sizes.Reverse ().ToArray (); } int maxSpaceBetweenItems = alignmentMode.FastHasFlags (AlignmentModes.AddSpaceBetweenItems) ? 1 : 0; int totalItemsSize = sizes.Sum (); int totalGaps = sizes.Length - 1; // total gaps between items int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spacesToGive if we had enough room int spacesToGive = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out if (totalItemsSize >= containerSize) { spacesToGive = 0; } else if (totalItemsAndSpaces > containerSize) { spacesToGive = containerSize - totalItemsSize; } AlignmentModes mode = alignmentMode & ~AlignmentModes.AddSpaceBetweenItems; // copy to avoid modifying the original switch (alignment) { case Alignment.Start: switch (mode) { case AlignmentModes.StartToEnd: return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive); case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast: return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); case AlignmentModes.EndToStart: return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast: return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ; } break; case Alignment.End: switch (mode) { case AlignmentModes.StartToEnd: return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast: return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); case AlignmentModes.EndToStart: return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast: return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ; } break; case Alignment.Center: mode &= ~AlignmentModes.IgnoreFirstOrLast; switch (mode) { case AlignmentModes.StartToEnd: return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); case AlignmentModes.EndToStart: return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); } break; case Alignment.Fill: mode &= ~AlignmentModes.IgnoreFirstOrLast; switch (mode) { case AlignmentModes.StartToEnd: return Fill (in sizesCopy, containerSize, totalItemsSize); case AlignmentModes.EndToStart: return Fill (in sizesCopy, containerSize, totalItemsSize).Reverse ().ToArray (); } break; default: throw new ArgumentOutOfRangeException (nameof (alignment), alignment, null); } return []; } internal static int [] Start (ref readonly int [] sizes, int maxSpaceBetweenItems, int spacesToGive) { var positions = new int [sizes.Length]; // positions of the items. the return value. for (var i = 0; i < sizes.Length; i++) { CheckSizeCannotBeNegative (i, in sizes); if (i == 0) { positions [0] = 0; // first item position continue; } int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; // subsequent items are placed one space after the previous item positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; } return positions; } internal static int [] IgnoreFirst ( ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive ) { var positions = new int [sizes.Length]; // positions of the items. the return value. if (sizes.Length > 1) { var currentPosition = 0; positions [0] = currentPosition; // first item is flush left for (int i = sizes.Length - 1; i >= 0; i--) { CheckSizeCannotBeNegative (i, in sizes); if (i == sizes.Length - 1) { // start at right currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i]; positions [i] = currentPosition; } if (i < sizes.Length - 1 && i > 0) { int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; positions [i] = currentPosition - sizes [i] - spaceBefore; currentPosition = positions [i]; } } } else if (sizes.Length == 1) { CheckSizeCannotBeNegative (0, in sizes); positions [0] = 0; // single item is flush left } return positions; } internal static int [] IgnoreLast ( ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive ) { var positions = new int [sizes.Length]; // positions of the items. the return value. if (sizes.Length > 1) { var currentPosition = 0; if (totalItemsSize > containerSize) { // Don't allow negative positions currentPosition = int.Max(0, containerSize - totalItemsSize - spacesToGive); } for (var i = 0; i < sizes.Length; i++) { CheckSizeCannotBeNegative (i, in sizes); if (i < sizes.Length - 1) { int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; positions [i] = currentPosition; currentPosition += sizes [i] + spaceBefore; } } positions [sizes.Length - 1] = containerSize - sizes [^1]; } else if (sizes.Length == 1) { CheckSizeCannotBeNegative (0, in sizes); positions [0] = containerSize - sizes [0]; // single item is flush right } return positions; } internal static int [] Fill (ref readonly int [] sizes, int containerSize, int totalItemsSize) { var positions = new int [sizes.Length]; // positions of the items. the return value. int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0; int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0; var currentPosition = 0; for (var i = 0; i < sizes.Length; i++) { CheckSizeCannotBeNegative (i, in sizes); positions [i] = currentPosition; int extraSpace = i < remainder ? 1 : 0; currentPosition += sizes [i] + spaceBetween + extraSpace; } return positions; } internal static int [] Center (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive) { var positions = new int [sizes.Length]; // positions of the items. the return value. if (sizes.Length > 1) { // remaining space to be distributed before first and after the items int remainingSpace = containerSize - totalItemsSize - spacesToGive; for (var i = 0; i < sizes.Length; i++) { CheckSizeCannotBeNegative (i, in sizes); if (i == 0) { positions [i] = remainingSpace / 2; // first item position continue; } int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; // subsequent items are placed one space after the previous item positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; } } else if (sizes.Length == 1) { CheckSizeCannotBeNegative (0, in sizes); positions [0] = (containerSize - sizes [0]) / 2; // single item is centered } return positions; } internal static int [] End (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive) { var positions = new int [sizes.Length]; // positions of the items. the return value. int currentPosition = containerSize - totalItemsSize - spacesToGive; for (var i = 0; i < sizes.Length; i++) { CheckSizeCannotBeNegative (i, in sizes); int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; positions [i] = currentPosition; currentPosition += sizes [i] + spaceBefore; } return positions; } private static void CheckSizeCannotBeNegative (int i, ref readonly int [] sizes) { if (sizes [i] < 0) { throw new ArgumentException ("The size of an item cannot be negative."); } } }