123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- #nullable enable
- using System.ComponentModel;
- using System.Text.RegularExpressions;
- namespace Terminal.Gui;
- /// <summary>
- /// Enables alignment of a set of views.
- /// </summary>
- /// <remarks>
- /// <para>
- /// Updating the properties of <see cref="Aligner"/> is supported, but will not automatically cause re-layout to
- /// happen. <see cref="View.Layout()"/>
- /// must be called on the SuperView.
- /// </para>
- /// <para>
- /// Views that should be aligned together must have a distinct <see cref="GroupId"/>. When only a single
- /// set of views is aligned within a SuperView, setting <see cref="GroupId"/> is optional because it defaults to 0.
- /// </para>
- /// <para>
- /// The first view added to the Superview with a given <see cref="GroupId"/> is used to determine the alignment of
- /// the group.
- /// The alignment is applied to all views with the same <see cref="GroupId"/>.
- /// </para>
- /// </remarks>
- public record PosAlign : Pos
- {
- /// <summary>
- /// The cached location. Used to store the calculated location to minimize recalculating it.
- /// </summary>
- internal int? _cachedLocation;
- private readonly Aligner? _aligner;
- /// <summary>
- /// Gets the alignment settings.
- /// </summary>
- 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.
- /// <summary>
- /// Returns the minimum size a group of views with the same <paramref name="groupId"/> can be.
- /// </summary>
- /// <param name="groupId"></param>
- /// <param name="views"></param>
- /// <param name="dimension"></param>
- /// <returns></returns>
- public static int CalculateMinDimension (int groupId, IList<View> views, Dimension dimension)
- {
- List<int> dimensionsList = new ();
- // PERF: If this proves a perf issue, consider caching a ref to this list in each item
- List<View> 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<PosAlign> (out PosAlign pos) => pos.GroupId == groupId,
- Dimension.Height when v.Y.Has<PosAlign> (out PosAlign pos) => pos.GroupId == groupId,
- _ => false
- };
- }
- /// <summary>
- /// 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 <see cref="GroupId"/> is not needed because it defaults to 0.
- /// </summary>
- public int GroupId { get; init; }
- /// <inheritdoc/>
- 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<View>? groupViews;
- if (us.SuperView is null)
- {
- groupViews = new List<View> ();
- 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; }
- /// <summary>
- /// Aligns the views in <paramref name="views"/> that have the same group ID as <paramref name="groupId"/>.
- /// Updates each view's cached _location.
- /// </summary>
- /// <param name="groupId"></param>
- /// <param name="views"></param>
- /// <param name="dimension"></param>
- /// <param name="size"></param>
- private static void AlignAndUpdateGroup (int groupId, IList<View> views, Dimension dimension, int size)
- {
- List<int> dimensionsList = new ();
- // PERF: If this proves a perf issue, consider caching a ref to this list in each item
- List<PosAlign?> 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; }
- }
|