123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- #nullable enable
- using System.Diagnostics;
- namespace Terminal.Gui;
- /// <summary>
- /// Represents a dimension that automatically sizes the view to fit all the view's Content, SubViews, and/or Text.
- /// </summary>
- /// <remarks>
- /// <para>
- /// See <see cref="DimAutoStyle"/>.
- /// </para>
- /// <para>
- /// This is a low-level API that is typically used internally by the layout system. Use the various static
- /// methods on the <see cref="Dim"/> class to create <see cref="Dim"/> objects instead.
- /// </para>
- /// </remarks>
- /// <param name="MaximumContentDim">The maximum dimension the View's ContentSize will be fit to.</param>
- /// <param name="MinimumContentDim">The minimum dimension the View's ContentSize will be constrained to.</param>
- /// <param name="Style">The <see cref="DimAutoStyle"/> of the <see cref="DimAuto"/>.</param>
- public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoStyle Style) : Dim
- {
- /// <inheritdoc/>
- public override string ToString () { return $"Auto({Style},{MinimumContentDim},{MaximumContentDim})"; }
- /// <inheritdoc />
- internal override int GetAnchor (int size) => 0;
- internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension)
- {
- var textSize = 0;
- var maxCalculatedSize = 0;
- int autoMin = MinimumContentDim?.GetAnchor (superviewContentSize) ?? 0;
- int screenX4 = dimension == Dimension.Width ? Application.Screen.Width * 4 : Application.Screen.Height * 4;
- int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? screenX4;
- Debug.Assert (autoMin <= autoMax, "MinimumContentDim must be less than or equal to MaximumContentDim.");
- if (Style.FastHasFlags (DimAutoStyle.Text))
- {
- if (dimension == Dimension.Width)
- {
- if (us.TextFormatter.ConstrainToWidth is null)
- {
- // Set BOTH width and height (by setting Size). We do this because we will be called again, next
- // for Dimension.Height. We need to know the width to calculate the height.
- us.TextFormatter.ConstrainToSize = us.TextFormatter.FormatAndGetSize (new (int.Min (autoMax, screenX4), screenX4));
- }
- textSize = us.TextFormatter.ConstrainToWidth!.Value;
- }
- else
- {
- if (us.TextFormatter.ConstrainToHeight is null)
- {
- // Set just the height. It is assumed that the width has already been set.
- // TODO: There may be cases where the width is not set. We may need to set it here.
- textSize = us.TextFormatter.FormatAndGetSize (new (us.TextFormatter.ConstrainToWidth ?? screenX4, int.Min (autoMax, screenX4))).Height;
- us.TextFormatter.ConstrainToHeight = textSize;
- }
- else
- {
- textSize = us.TextFormatter.ConstrainToHeight.Value;
- }
- }
- }
- List<View> viewsNeedingLayout = new ();
- if (Style.FastHasFlags (DimAutoStyle.Content))
- {
- maxCalculatedSize = textSize;
- if (us is { ContentSizeTracksViewport: false, Subviews.Count: 0 })
- {
- // ContentSize was explicitly set. Use `us.ContentSize` to determine size.
- maxCalculatedSize = dimension == Dimension.Width ? us.GetContentSize ().Width : us.GetContentSize ().Height;
- }
- else
- {
- // TOOD: All the below is a naive implementation. It may be possible to optimize this.
- List<View> includedSubviews = us.Subviews.ToList ();
- // If [x] it can cause `us.ContentSize` to change.
- // If [ ] it doesn't need special processing for us to determine `us.ContentSize`.
- // -------------------- Pos types that are dependent on `us.Subviews`
- // [ ] PosAlign - Position is dependent on other views with `GroupId` AND `us.ContentSize`
- // [x] PosView - Position is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
- // [x] PosCombine - Position is dependent if `Pos.Has [one of the above]` - it can cause a change in `us.ContentSize`
- // -------------------- Pos types that are dependent on `us.ContentSize`
- // [ ] PosAlign - Position is dependent on other views with `GroupId` AND `us.ContentSize`
- // [x] PosAnchorEnd - Position is dependent on `us.ContentSize` AND `subview.Frame` - it can cause a change in `us.ContentSize`
- // [ ] PosCenter - Position is dependent `us.ContentSize` AND `subview.Frame` -
- // [ ] PosPercent - Position is dependent `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
- // [x] PosCombine - Position is dependent if `Pos.Has [one of the above]` - it can cause a change in `us.ContentSize`
- // -------------------- Pos types that are not dependent on either `us.Subviews` or `us.ContentSize`
- // [ ] PosAbsolute - Position is fixed.
- // [ ] PosFunc - Position is internally calculated.
- // -------------------- Dim types that are dependent on `us.Subviews`
- // [x] DimView - Dimension is dependent on `subview.Target`
- // [x] DimCombine - Dimension is dependent if `Dim.Has [one of the above]` - it can cause a change in `us.ContentSize`
- // -------------------- Dim types that are dependent on `us.ContentSize`
- // [ ] DimFill - Dimension is dependent on `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
- // [ ] DimPercent - Dimension is dependent on `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
- // [ ] DimCombine - Dimension is dependent if `Dim.Has [one of the above]`
- // -------------------- Dim types that are not dependent on either `us.Subviews` or `us.ContentSize`
- // [ ] DimAuto - Dimension is internally calculated
- // [ ] DimAbsolute - Dimension is fixed
- // [ ] DimFunc - Dimension is internally calculated
- // ======================================================
- // Do the easy stuff first - subviews whose position and size are not dependent on other views or content size
- // ======================================================
- // [ ] PosAbsolute - Position is fixed.
- // [ ] PosFunc - Position is internally calculated
- // [ ] DimAuto - Dimension is internally calculated
- // [ ] DimAbsolute - Dimension is fixed
- // [ ] DimFunc - Dimension is internally calculated
- List<View> notDependentSubViews;
- if (dimension == Dimension.Width)
- {
- notDependentSubViews = includedSubviews.Where (
- v => v.Width is { }
- && (v.X is PosAbsolute or PosFunc
- || v.Width is DimAuto
- or DimAbsolute
- or DimFunc) // BUGBUG: We should use v.X.Has and v.Width.Has?
- && !v.X.Has<PosAnchorEnd> (out _)
- && !v.X.Has<PosAlign> (out _)
- && !v.X.Has<PosCenter> (out _)
- && !v.Width.Has<DimFill> (out _)
- && !v.Width.Has<DimPercent> (out _)
- )
- .ToList ();
- }
- else
- {
- notDependentSubViews = includedSubviews.Where (
- v => v.Height is { }
- && (v.Y is PosAbsolute or PosFunc
- || v.Height is DimAuto
- or DimAbsolute
- or DimFunc) // BUGBUG: We should use v.Y.Has and v.Height.Has?
- && !v.Y.Has<PosAnchorEnd> (out _)
- && !v.Y.Has<PosAlign> (out _)
- && !v.Y.Has<PosCenter> (out _)
- && !v.Height.Has<DimFill> (out _)
- && !v.Height.Has<DimPercent> (out _)
- )
- .ToList ();
- }
- foreach (View notDependentSubView in notDependentSubViews)
- {
- notDependentSubView.SetRelativeLayout (us.GetContentSize ());
- }
- for (var i = 0; i < notDependentSubViews.Count; i++)
- {
- View v = notDependentSubViews [i];
- var size = 0;
- if (dimension == Dimension.Width)
- {
- int width = v.Width!.Calculate (0, superviewContentSize, v, dimension);
- size = v.X.GetAnchor (0) + width;
- }
- else
- {
- int height = v.Height!.Calculate (0, superviewContentSize, v, dimension);
- size = v.Y!.GetAnchor (0) + height;
- }
- if (size > maxCalculatedSize)
- {
- maxCalculatedSize = size;
- }
- }
- // ************** We now have some idea of `us.ContentSize` ***************
- #region Centered
- // [ ] PosCenter - Position is dependent `us.ContentSize` AND `subview.Frame`
- List<View> centeredSubViews;
- if (dimension == Dimension.Width)
- {
- centeredSubViews = us.Subviews.Where (v => v.X.Has<PosCenter> (out _)).ToList ();
- }
- else
- {
- centeredSubViews = us.Subviews.Where (v => v.Y.Has<PosCenter> (out _)).ToList ();
- }
- viewsNeedingLayout.AddRange (centeredSubViews);
- var maxCentered = 0;
- for (var i = 0; i < centeredSubViews.Count; i++)
- {
- View v = centeredSubViews [i];
- if (dimension == Dimension.Width)
- {
- int width = v.Width!.Calculate (0, screenX4, v, dimension);
- maxCentered = v.X.GetAnchor (0) + width;
- }
- else
- {
- int height = v.Height!.Calculate (0, screenX4, v, dimension);
- maxCentered = v.Y.GetAnchor (0) + height;
- }
- }
- maxCalculatedSize = int.Max (maxCalculatedSize, maxCentered);
- #endregion Centered
- #region Percent
- // [ ] DimPercent - Dimension is dependent on `us.ContentSize`
- // No need to do anything.
- #endregion Percent
- #region Aligned
- // [ ] PosAlign - Position is dependent on other views with `GroupId` AND `us.ContentSize`
- var maxAlign = 0;
- // Use Linq to get a list of distinct GroupIds from the subviews
- List<int> groupIds = includedSubviews.Select (
- v =>
- {
- return dimension switch
- {
- Dimension.Width when v.X.Has<PosAlign> (out PosAlign posAlign) =>
- ((PosAlign)posAlign).GroupId,
- Dimension.Height when v.Y.Has<PosAlign> (out PosAlign posAlign) =>
- ((PosAlign)posAlign).GroupId,
- _ => -1
- };
- })
- .Distinct ()
- .ToList ();
- foreach (int groupId in groupIds.Where (g => g != -1))
- {
- // PERF: If this proves a perf issue, consider caching a ref to this list in each item
- List<PosAlign?> posAlignsInGroup = includedSubviews.Where (v => PosAlign.HasGroupId (v, dimension, groupId))
- .Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign)
- .ToList ();
- if (posAlignsInGroup.Count == 0)
- {
- continue;
- }
- maxAlign = PosAlign.CalculateMinDimension (groupId, includedSubviews, dimension);
- }
- maxCalculatedSize = int.Max (maxCalculatedSize, maxAlign);
- #endregion Aligned
- #region Anchored
- // [x] PosAnchorEnd - Position is dependent on `us.ContentSize` AND `subview.Frame`
- List<View> anchoredSubViews;
- if (dimension == Dimension.Width)
- {
- anchoredSubViews = includedSubviews.Where (v => v.X.Has<PosAnchorEnd> (out _)).ToList ();
- }
- else
- {
- anchoredSubViews = includedSubviews.Where (v => v.Y.Has<PosAnchorEnd> (out _)).ToList ();
- }
- viewsNeedingLayout.AddRange (anchoredSubViews);
- var maxAnchorEnd = 0;
- for (var i = 0; i < anchoredSubViews.Count; i++)
- {
- View v = anchoredSubViews [i];
- // Need to set the relative layout for PosAnchorEnd subviews to calculate the size
- // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL).
- if (dimension == Dimension.Width)
- {
- v.SetRelativeLayout (new (maxCalculatedSize, screenX4));
- }
- else
- {
- v.SetRelativeLayout (new (screenX4, maxCalculatedSize));
- }
- maxAnchorEnd = dimension == Dimension.Width
- ? v.X.GetAnchor (maxCalculatedSize + v.Frame.Width)
- : v.Y.GetAnchor (maxCalculatedSize + v.Frame.Height);
- }
- maxCalculatedSize = Math.Max (maxCalculatedSize, maxAnchorEnd);
- #endregion Anchored
- #region PosView
- // [x] PosView - Position is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
- List<View> posViewSubViews;
- if (dimension == Dimension.Width)
- {
- posViewSubViews = includedSubviews.Where (v => v.X.Has<PosView> (out _)).ToList ();
- }
- else
- {
- posViewSubViews = includedSubviews.Where (v => v.Y.Has<PosView> (out _)).ToList ();
- }
- for (var i = 0; i < posViewSubViews.Count; i++)
- {
- View v = posViewSubViews [i];
- // BUGBUG: The order may not be correct. May need to call TopologicalSort?
- // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL).
- int maxPosView = dimension == Dimension.Width
- ? v.Frame.X + v.Width!.Calculate (0, maxCalculatedSize, v, dimension)
- : v.Frame.Y + v.Height!.Calculate (0, maxCalculatedSize, v, dimension);
- if (maxPosView > maxCalculatedSize)
- {
- maxCalculatedSize = maxPosView;
- }
- }
- #endregion PosView
- // [x] PosCombine - Position is dependent if `Pos.Has ([one of the above]` - it can cause a change in `us.ContentSize`
- #region DimView
- // [x] DimView - Dimension is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
- List<View> dimViewSubViews;
- if (dimension == Dimension.Width)
- {
- dimViewSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has<DimView> (out _)).ToList ();
- }
- else
- {
- dimViewSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has<DimView> (out _)).ToList ();
- }
- for (var i = 0; i < dimViewSubViews.Count; i++)
- {
- View v = dimViewSubViews [i];
- // BUGBUG: The order may not be correct. May need to call TopologicalSort?
- // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL).
- int maxDimView = dimension == Dimension.Width
- ? v.Frame.X + v.Width!.Calculate (0, maxCalculatedSize, v, dimension)
- : v.Frame.Y + v.Height!.Calculate (0, maxCalculatedSize, v, dimension);
- if (maxDimView > maxCalculatedSize)
- {
- maxCalculatedSize = maxDimView;
- }
- }
- #endregion DimView
- #region DimAuto
- // [ ] DimAuto - Dimension is internally calculated
- List<View> dimAutoSubViews;
- if (dimension == Dimension.Width)
- {
- dimAutoSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has<DimAuto> (out _)).ToList ();
- }
- else
- {
- dimAutoSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has<DimAuto> (out _)).ToList ();
- }
- for (var i = 0; i < dimAutoSubViews.Count; i++)
- {
- View v = dimAutoSubViews [i];
- int maxDimAuto = dimension == Dimension.Width
- ? v.Frame.X + v.Width!.Calculate (0, maxCalculatedSize, v, dimension)
- : v.Frame.Y + v.Height!.Calculate (0, maxCalculatedSize, v, dimension);
- if (maxDimAuto > maxCalculatedSize)
- {
- maxCalculatedSize = maxDimAuto;
- }
- }
- #endregion
- #region DimFill
- //// [ ] DimFill - Dimension is internally calculated
- //List<View> DimFillSubViews;
- //if (dimension == Dimension.Width)
- //{
- // DimFillSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has<DimFill> (out _)).ToList ();
- //}
- //else
- //{
- // DimFillSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has<DimFill> (out _)).ToList ();
- //}
- //for (var i = 0; i < DimFillSubViews.Count; i++)
- //{
- // View v = DimFillSubViews [i];
- // if (dimension == Dimension.Width)
- // {
- // v.SetRelativeLayout (new (maxCalculatedSize, 0));
- // }
- // else
- // {
- // v.SetRelativeLayout (new (0, maxCalculatedSize));
- // }
- // int maxDimFill = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height;
- // if (maxDimFill > maxCalculatedSize)
- // {
- // maxCalculatedSize = maxDimFill;
- // }
- //}
- #endregion
- }
- }
- // All sizes here are content-relative; ignoring adornments.
- // We take the largest of text and content.
- int max = int.Max (textSize, maxCalculatedSize);
- // And, if min: is set, it wins if larger
- max = int.Max (max, autoMin);
- // And, if max: is set, it wins if smaller
- max = int.Min (max, autoMax);
- Thickness thickness = us.GetAdornmentsThickness ();
- int adornmentThickness = dimension switch
- {
- Dimension.Width => thickness.Horizontal,
- Dimension.Height => thickness.Vertical,
- Dimension.None => 0,
- _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null)
- };
- max += adornmentThickness;
- return max;
- }
- }
|