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