#nullable enable
using System.ComponentModel;
using System.Text.RegularExpressions;
namespace Terminal.Gui;
///
/// Enables alignment of a set of views.
///
///
///
/// Updating the properties of is supported, but will not automatically cause re-layout to
/// happen.
/// must be called on the SuperView.
///
///
/// Views that should be aligned together must have a distinct . When only a single
/// set of views is aligned within a SuperView, setting is optional because it defaults to 0.
///
///
/// The first view added to the Superview with a given is used to determine the alignment of
/// the group.
/// The alignment is applied to all views with the same .
///
///
public record PosAlign : Pos
{
///
/// The cached location. Used to store the calculated location to minimize recalculating it.
///
internal int? _cachedLocation;
private readonly Aligner? _aligner;
///
/// Gets the alignment settings.
///
public required Aligner Aligner
{
get => _aligner!;
init
{
if (_aligner is { })
{
_aligner.PropertyChanged -= Aligner_PropertyChanged;
}
_aligner = value;
_aligner.PropertyChanged += Aligner_PropertyChanged;
}
}
// TODO: PosAlign.CalculateMinDimension is a hack. Need to figure out a better way of doing this.
///
/// Returns the minimum size a group of views with the same can be.
///
///
///
///
///
public static int CalculateMinDimension (int groupId, IList views, Dimension dimension)
{
List dimensionsList = new ();
// PERF: If this proves a perf issue, consider caching a ref to this list in each item
List viewsInGroup = views.Where (v => HasGroupId (v, dimension, groupId)).ToList ();
if (viewsInGroup.Count == 0)
{
return 0;
}
// PERF: We iterate over viewsInGroup multiple times here.
// Update the dimensionList with the sizes of the views
for (var index = 0; index < viewsInGroup.Count; index++)
{
View view = viewsInGroup [index];
PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
if (posAlign is { })
{
dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
}
}
// Align
return dimensionsList.Sum ();
}
internal static bool HasGroupId (View v, Dimension dimension, int groupId)
{
return dimension switch
{
Dimension.Width when v.X.Has (out PosAlign pos) => pos.GroupId == groupId,
Dimension.Height when v.Y.Has (out PosAlign pos) => pos.GroupId == groupId,
_ => false
};
}
///
/// Gets the identifier of a set of views that should be aligned together. When only a single
/// set of views in a SuperView is aligned, setting is not needed because it defaults to 0.
///
public int GroupId { get; init; }
///
public override string ToString () { return $"Align(alignment={Aligner.Alignment},modes={Aligner.AlignmentModes},groupId={GroupId})"; }
internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension)
{
if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension && !us.NeedsLayout)
{
return _cachedLocation.Value;
}
IList? groupViews;
if (us.SuperView is null)
{
groupViews = new List ();
groupViews.Add (us);
}
else
{
groupViews = us.SuperView!.Subviews.Where (v => HasGroupId (v, dimension, GroupId)).ToList ();
}
AlignAndUpdateGroup (GroupId, groupViews, dimension, superviewDimension);
if (_cachedLocation.HasValue)
{
return _cachedLocation.Value;
}
return 0;
}
internal override int GetAnchor (int width) { return _cachedLocation ?? 0 - width; }
///
/// Aligns the views in that have the same group ID as .
/// Updates each view's cached _location.
///
///
///
///
///
private static void AlignAndUpdateGroup (int groupId, IList views, Dimension dimension, int size)
{
List dimensionsList = new ();
// PERF: If this proves a perf issue, consider caching a ref to this list in each item
List posAligns = views.Where (v => PosAlign.HasGroupId (v, dimension, groupId))
.Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign)
.ToList ();
// PERF: We iterate over viewsInGroup multiple times here.
Aligner? firstInGroup = null;
// Update the dimensionList with the sizes of the views
for (var index = 0; index < posAligns.Count; index++)
{
if (posAligns [index] is { })
{
if (firstInGroup is null)
{
firstInGroup = posAligns [index]!.Aligner;
}
dimensionsList.Add (dimension == Dimension.Width
? views [index].Width!.Calculate(0, size, views [index], dimension)
: views [index].Height!.Calculate (0, size, views [index], dimension));
}
}
if (firstInGroup is null)
{
return;
}
// Update the first item in the group with the new container size.
firstInGroup.ContainerSize = size;
// Align
int [] locations = firstInGroup.Align (dimensionsList.ToArray ());
// Update the cached location for each item
for (int posIndex = 0, locIndex = 0; posIndex < posAligns.Count; posIndex++)
{
if (posAligns [posIndex] is { })
{
posAligns [posIndex]!._cachedLocation = locations [locIndex++];
}
}
}
private void Aligner_PropertyChanged (object? sender, PropertyChangedEventArgs e) { _cachedLocation = null; }
}