#nullable enable using System.ComponentModel; 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. /// public 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 => { return dimension switch { Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId, Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId, _ => false }; }) .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 (); } /// /// 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) { return _cachedLocation.Value; } if (us?.SuperView is null) { return 0; } AlignAndUpdateGroup (GroupId, us.SuperView.Subviews, 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.Select ( v => { switch (dimension) { case Dimension.Width when v.X.Has (typeof (PosAlign), out Pos pos): if (pos is PosAlign posAlignX && posAlignX.GroupId == groupId) { return posAlignX; } break; case Dimension.Height when v.Y.Has (typeof (PosAlign), out Pos pos): if (pos is PosAlign posAlignY && posAlignY.GroupId == groupId) { return posAlignY; } break; } return null; }) .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].Frame.Width : views [index].Frame.Height); } } 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; } }