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