#nullable enable
using System.Diagnostics;
namespace Terminal.Gui;
///
/// Represents a dimension that automatically sizes the view to fit all the view's Content, SubViews, and/or Text.
///
///
///
/// See .
///
///
/// This is a low-level API that is typically used internally by the layout system. Use the various static
/// methods on the class to create objects instead.
///
///
/// The maximum dimension the View's ContentSize will be fit to.
/// The minimum dimension the View's ContentSize will be constrained to.
/// The of the .
public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoStyle Style) : Dim
{
///
public override string ToString () { return $"Auto({Style},{MinimumContentDim},{MaximumContentDim})"; }
///
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 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 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 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 (out _)
&& !v.X.Has (out _)
&& !v.X.Has (out _)
&& !v.Width.Has (out _)
&& !v.Width.Has (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 (out _)
&& !v.Y.Has (out _)
&& !v.Y.Has (out _)
&& !v.Height.Has (out _)
&& !v.Height.Has (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 centeredSubViews;
if (dimension == Dimension.Width)
{
centeredSubViews = us.Subviews.Where (v => v.X.Has (out _)).ToList ();
}
else
{
centeredSubViews = us.Subviews.Where (v => v.Y.Has (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 groupIds = includedSubviews.Select (
v =>
{
return dimension switch
{
Dimension.Width when v.X.Has (out PosAlign posAlign) =>
((PosAlign)posAlign).GroupId,
Dimension.Height when v.Y.Has (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 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 anchoredSubViews;
if (dimension == Dimension.Width)
{
anchoredSubViews = includedSubviews.Where (v => v.X.Has (out _)).ToList ();
}
else
{
anchoredSubViews = includedSubviews.Where (v => v.Y.Has (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 posViewSubViews;
if (dimension == Dimension.Width)
{
posViewSubViews = includedSubviews.Where (v => v.X.Has (out _)).ToList ();
}
else
{
posViewSubViews = includedSubviews.Where (v => v.Y.Has (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 dimViewSubViews;
if (dimension == Dimension.Width)
{
dimViewSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has (out _)).ToList ();
}
else
{
dimViewSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has (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 dimAutoSubViews;
if (dimension == Dimension.Width)
{
dimAutoSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has (out _)).ToList ();
}
else
{
dimAutoSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has (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 DimFillSubViews;
//if (dimension == Dimension.Width)
//{
// DimFillSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has (out _)).ToList ();
//}
//else
//{
// DimFillSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has (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;
}
}