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