123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- using System.ComponentModel;
- namespace Terminal.Gui;
- /// <summary>
- /// Aligns items within a container based on the specified <see cref="Gui.Alignment"/>. Both horizontal and vertical
- /// alignments are supported.
- /// </summary>
- public class Aligner : INotifyPropertyChanged
- {
- private Alignment _alignment;
- /// <summary>
- /// Gets or sets how the <see cref="Aligner"/> aligns items within a container.
- /// </summary>
- /// <remarks>
- /// <para>
- /// <see cref="AlignmentModes"/> provides additional options for aligning items in a container.
- /// </para>
- /// </remarks>
- public Alignment Alignment
- {
- get => _alignment;
- set
- {
- _alignment = value;
- PropertyChanged?.Invoke (this, new (nameof (Alignment)));
- }
- }
- private AlignmentModes _alignmentMode = AlignmentModes.StartToEnd;
- /// <summary>
- /// Gets or sets the modes controlling <see cref="Alignment"/>.
- /// </summary>
- public AlignmentModes AlignmentModes
- {
- get => _alignmentMode;
- set
- {
- _alignmentMode = value;
- PropertyChanged?.Invoke (this, new (nameof (AlignmentModes)));
- }
- }
- private int _containerSize;
- /// <summary>
- /// The size of the container.
- /// </summary>
- public int ContainerSize
- {
- get => _containerSize;
- set
- {
- _containerSize = value;
- PropertyChanged?.Invoke (this, new (nameof (ContainerSize)));
- }
- }
- /// <inheritdoc/>
- public event PropertyChangedEventHandler PropertyChanged;
- /// <summary>
- /// Takes a list of item sizes and returns a list of the positions of those items when aligned within
- /// <see name="ContainerSize"/>
- /// using the <see cref="Alignment"/> and <see cref="AlignmentModes"/> settings.
- /// </summary>
- /// <param name="sizes">The sizes of the items to align.</param>
- /// <returns>The locations of the items, from left/top to right/bottom.</returns>
- public int [] Align (int [] sizes) { return Align (Alignment, AlignmentModes, ContainerSize, sizes); }
- /// <summary>
- /// Takes a list of item sizes and returns a list of the positions of those items when aligned within
- /// <paramref name="containerSize"/>
- /// using specified parameters.
- /// </summary>
- /// <param name="alignment">Specifies how the items will be aligned.</param>
- /// <param name="alignmentMode"></param>
- /// <param name="containerSize">The size of the container.</param>
- /// <param name="sizes">The sizes of the items to align.</param>
- /// <returns>The positions of the items, from left/top to right/bottom.</returns>
- 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.");
- }
- }
- }
|