DimAuto.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. #nullable enable
  2. using System.Diagnostics;
  3. namespace Terminal.Gui;
  4. /// <summary>
  5. /// Represents a dimension that automatically sizes the view to fit all the view's Content, SubViews, and/or Text.
  6. /// </summary>
  7. /// <remarks>
  8. /// <para>
  9. /// See <see cref="DimAutoStyle"/>.
  10. /// </para>
  11. /// <para>
  12. /// This is a low-level API that is typically used internally by the layout system. Use the various static
  13. /// methods on the <see cref="Dim"/> class to create <see cref="Dim"/> objects instead.
  14. /// </para>
  15. /// </remarks>
  16. public class DimAuto : Dim
  17. {
  18. /// <inheritdoc />
  19. public override bool Equals (object? other)
  20. {
  21. if (ReferenceEquals (this, other))
  22. {
  23. return true;
  24. }
  25. if (other is not DimAuto auto)
  26. {
  27. return false;
  28. }
  29. return EqualityComparer<Dim?>.Default.Equals (MinimumContentDim, auto.MinimumContentDim) &&
  30. EqualityComparer<Dim?>.Default.Equals (MaximumContentDim, auto.MaximumContentDim) &&
  31. Style == auto.Style;
  32. }
  33. /// <inheritdoc />
  34. public override int GetHashCode ()
  35. {
  36. return HashCode.Combine (MinimumContentDim, MaximumContentDim, Style);
  37. }
  38. private readonly Dim? _maximumContentDim;
  39. /// <summary>
  40. /// Gets the maximum dimension the View's ContentSize will be fit to. NOT CURRENTLY SUPPORTED.
  41. /// </summary>
  42. // ReSharper disable once ConvertToAutoProperty
  43. public required Dim? MaximumContentDim
  44. {
  45. get => _maximumContentDim;
  46. init => _maximumContentDim = value;
  47. }
  48. private readonly Dim? _minimumContentDim;
  49. /// <summary>
  50. /// Gets the minimum dimension the View's ContentSize will be constrained to.
  51. /// </summary>
  52. // ReSharper disable once ConvertToAutoProperty
  53. public required Dim? MinimumContentDim
  54. {
  55. get => _minimumContentDim;
  56. init => _minimumContentDim = value;
  57. }
  58. private readonly DimAutoStyle _style;
  59. /// <summary>
  60. /// Gets the style of the DimAuto.
  61. /// </summary>
  62. // ReSharper disable once ConvertToAutoProperty
  63. public required DimAutoStyle Style
  64. {
  65. get => _style;
  66. init => _style = value;
  67. }
  68. /// <inheritdoc/>
  69. public override string ToString () { return $"Auto({Style},{MinimumContentDim},{MaximumContentDim})"; }
  70. internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension)
  71. {
  72. var textSize = 0;
  73. var maxCalculatedSize = 0;
  74. int autoMin = MinimumContentDim?.GetAnchor (superviewContentSize) ?? 0;
  75. int screenX4 = dimension == Dimension.Width ? Application.Screen.Width * 4 : Application.Screen.Height * 4;
  76. int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? screenX4;
  77. Debug.Assert (autoMin <= autoMax, "MinimumContentDim must be less than or equal to MaximumContentDim.");
  78. if (Style.FastHasFlags (DimAutoStyle.Text))
  79. {
  80. if (dimension == Dimension.Width)
  81. {
  82. if (us.TextFormatter.ConstrainToWidth is null)
  83. {
  84. // Set BOTH width and height (by setting Size). We do this because we will be called again, next
  85. // for Dimension.Height. We need to know the width to calculate the height.
  86. us.TextFormatter.ConstrainToSize = us.TextFormatter.FormatAndGetSize (new (int.Min (autoMax, screenX4), screenX4));
  87. }
  88. textSize = us.TextFormatter.ConstrainToWidth!.Value;
  89. }
  90. else
  91. {
  92. if (us.TextFormatter.ConstrainToHeight is null)
  93. {
  94. // Set just the height. It is assumed that the width has already been set.
  95. // TODO: There may be cases where the width is not set. We may need to set it here.
  96. textSize = us.TextFormatter.FormatAndGetSize (new (us.TextFormatter.ConstrainToWidth ?? screenX4, int.Min (autoMax, screenX4))).Height;
  97. us.TextFormatter.ConstrainToHeight = textSize;
  98. }
  99. else
  100. {
  101. textSize = us.TextFormatter.ConstrainToHeight.Value;
  102. }
  103. }
  104. }
  105. List<View> viewsNeedingLayout = new ();
  106. if (Style.FastHasFlags (DimAutoStyle.Content))
  107. {
  108. if (!us.ContentSizeTracksViewport)
  109. {
  110. // ContentSize was explicitly set. Use `us.ContentSize` to determine size.
  111. maxCalculatedSize = dimension == Dimension.Width ? us.GetContentSize ().Width : us.GetContentSize ().Height;
  112. }
  113. else
  114. {
  115. maxCalculatedSize = textSize;
  116. // TOOD: All the below is a naive implementation. It may be possible to optimize this.
  117. List<View> includedSubviews = us.Subviews.ToList ();
  118. // If [x] it can cause `us.ContentSize` to change.
  119. // If [ ] it doesn't need special processing for us to determine `us.ContentSize`.
  120. // -------------------- Pos types that are dependent on `us.Subviews`
  121. // [ ] PosAlign - Position is dependent on other views with `GroupId` AND `us.ContentSize`
  122. // [x] PosView - Position is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
  123. // [x] PosCombine - Position is dependent if `Pos.Has [one of the above]` - it can cause a change in `us.ContentSize`
  124. // -------------------- Pos types that are dependent on `us.ContentSize`
  125. // [ ] PosAlign - Position is dependent on other views with `GroupId` AND `us.ContentSize`
  126. // [x] PosAnchorEnd - Position is dependent on `us.ContentSize` AND `subview.Frame` - it can cause a change in `us.ContentSize`
  127. // [ ] PosCenter - Position is dependent `us.ContentSize` AND `subview.Frame` -
  128. // [ ] PosPercent - Position is dependent `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
  129. // [x] PosCombine - Position is dependent if `Pos.Has [one of the above]` - it can cause a change in `us.ContentSize`
  130. // -------------------- Pos types that are not dependent on either `us.Subviews` or `us.ContentSize`
  131. // [ ] PosAbsolute - Position is fixed.
  132. // [ ] PosFunc - Position is internally calculated.
  133. // -------------------- Dim types that are dependent on `us.Subviews`
  134. // [x] DimView - Dimension is dependent on `subview.Target`
  135. // [x] DimCombine - Dimension is dependent if `Dim.Has [one of the above]` - it can cause a change in `us.ContentSize`
  136. // -------------------- Dim types that are dependent on `us.ContentSize`
  137. // [ ] DimFill - Dimension is dependent on `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
  138. // [ ] DimPercent - Dimension is dependent on `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
  139. // [ ] DimCombine - Dimension is dependent if `Dim.Has [one of the above]`
  140. // -------------------- Dim types that are not dependent on either `us.Subviews` or `us.ContentSize`
  141. // [ ] DimAuto - Dimension is internally calculated
  142. // [ ] DimAbsolute - Dimension is fixed
  143. // [ ] DimFunc - Dimension is internally calculated
  144. // ======================================================
  145. // Do the easy stuff first - subviews whose position and size are not dependent on other views or content size
  146. // ======================================================
  147. // [ ] PosAbsolute - Position is fixed.
  148. // [ ] PosFunc - Position is internally calculated
  149. // [ ] DimAuto - Dimension is internally calculated
  150. // [ ] DimAbsolute - Dimension is fixed
  151. // [ ] DimFunc - Dimension is internally calculated
  152. List<View> notDependentSubViews;
  153. if (dimension == Dimension.Width)
  154. {
  155. notDependentSubViews = includedSubviews.Where (
  156. v => v.Width is { }
  157. && (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?
  158. && !v.X.Has (typeof (PosAnchorEnd), out _)
  159. && !v.X.Has (typeof (PosAlign), out _)
  160. && !v.X.Has (typeof (PosCenter), out _)
  161. && !v.Width.Has (typeof (DimFill), out _)
  162. && !v.Width.Has (typeof (DimPercent), out _)
  163. )
  164. .ToList ();
  165. }
  166. else
  167. {
  168. notDependentSubViews = includedSubviews.Where (
  169. v => v.Height is { }
  170. && (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?
  171. && !v.Y.Has (typeof (PosAnchorEnd), out _)
  172. && !v.Y.Has (typeof (PosAlign), out _)
  173. && !v.Y.Has (typeof (PosCenter), out _)
  174. && !v.Height.Has (typeof (DimFill), out _)
  175. && !v.Height.Has (typeof (DimPercent), out _)
  176. )
  177. .ToList ();
  178. }
  179. for (var i = 0; i < notDependentSubViews.Count; i++)
  180. {
  181. View v = notDependentSubViews [i];
  182. var size = 0;
  183. if (dimension == Dimension.Width)
  184. {
  185. int width = v.Width!.Calculate (0, superviewContentSize, v, dimension);
  186. size = v.X.GetAnchor (0) + width;
  187. }
  188. else
  189. {
  190. int height = v.Height!.Calculate (0, superviewContentSize, v, dimension);
  191. size = v.Y!.GetAnchor (0) + height;
  192. }
  193. if (size > maxCalculatedSize)
  194. {
  195. maxCalculatedSize = size;
  196. }
  197. }
  198. // ************** We now have some idea of `us.ContentSize` ***************
  199. #region Centered
  200. // [ ] PosCenter - Position is dependent `us.ContentSize` AND `subview.Frame`
  201. List<View> centeredSubViews;
  202. if (dimension == Dimension.Width)
  203. {
  204. centeredSubViews = us.Subviews.Where (v => v.X.Has (typeof (PosCenter), out _)).ToList ();
  205. }
  206. else
  207. {
  208. centeredSubViews = us.Subviews.Where (v => v.Y.Has (typeof (PosCenter), out _)).ToList ();
  209. }
  210. viewsNeedingLayout.AddRange (centeredSubViews);
  211. var maxCentered = 0;
  212. for (var i = 0; i < centeredSubViews.Count; i++)
  213. {
  214. View v = centeredSubViews [i];
  215. if (dimension == Dimension.Width)
  216. {
  217. int width = v.Width!.Calculate (0, screenX4, v, dimension);
  218. maxCentered = v.X.GetAnchor (0) + width;
  219. }
  220. else
  221. {
  222. int height = v.Height!.Calculate (0, screenX4, v, dimension);
  223. maxCentered = v.Y.GetAnchor (0) + height;
  224. }
  225. }
  226. maxCalculatedSize = int.Max (maxCalculatedSize, maxCentered);
  227. #endregion Centered
  228. #region Percent
  229. // [ ] DimPercent - Dimension is dependent on `us.ContentSize`
  230. // No need to do anything.
  231. #endregion Percent
  232. #region Aligned
  233. // [ ] PosAlign - Position is dependent on other views with `GroupId` AND `us.ContentSize`
  234. var maxAlign = 0;
  235. // Use Linq to get a list of distinct GroupIds from the subviews
  236. List<int> groupIds = includedSubviews.Select (
  237. v =>
  238. {
  239. if (dimension == Dimension.Width)
  240. {
  241. if (v.X.Has (typeof (PosAlign), out Pos posAlign))
  242. {
  243. return ((PosAlign)posAlign).GroupId;
  244. }
  245. }
  246. else
  247. {
  248. if (v.Y.Has (typeof (PosAlign), out Pos posAlign))
  249. {
  250. return ((PosAlign)posAlign).GroupId;
  251. }
  252. }
  253. return -1;
  254. })
  255. .Distinct ()
  256. .ToList ();
  257. foreach (int groupId in groupIds.Where (g => g != -1))
  258. {
  259. // PERF: If this proves a perf issue, consider caching a ref to this list in each item
  260. List<PosAlign?> posAlignsInGroup = includedSubviews.Where (
  261. v =>
  262. {
  263. return dimension switch
  264. {
  265. Dimension.Width when v.X is PosAlign alignX => alignX.GroupId
  266. == groupId,
  267. Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId
  268. == groupId,
  269. _ => false
  270. };
  271. })
  272. .Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign)
  273. .ToList ();
  274. if (posAlignsInGroup.Count == 0)
  275. {
  276. continue;
  277. }
  278. maxAlign = PosAlign.CalculateMinDimension (groupId, includedSubviews, dimension);
  279. }
  280. maxCalculatedSize = int.Max (maxCalculatedSize, maxAlign);
  281. #endregion Aligned
  282. #region Anchored
  283. // [x] PosAnchorEnd - Position is dependent on `us.ContentSize` AND `subview.Frame`
  284. List<View> anchoredSubViews;
  285. if (dimension == Dimension.Width)
  286. {
  287. anchoredSubViews = includedSubviews.Where (v => v.X.Has (typeof (PosAnchorEnd), out _)).ToList ();
  288. }
  289. else
  290. {
  291. anchoredSubViews = includedSubviews.Where (v => v.Y.Has (typeof (PosAnchorEnd), out _)).ToList ();
  292. }
  293. viewsNeedingLayout.AddRange (anchoredSubViews);
  294. var maxAnchorEnd = 0;
  295. for (var i = 0; i < anchoredSubViews.Count; i++)
  296. {
  297. View v = anchoredSubViews [i];
  298. // Need to set the relative layout for PosAnchorEnd subviews to calculate the size
  299. // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL).
  300. if (dimension == Dimension.Width)
  301. {
  302. v.SetRelativeLayout (new (maxCalculatedSize, screenX4));
  303. }
  304. else
  305. {
  306. v.SetRelativeLayout (new (screenX4, maxCalculatedSize));
  307. }
  308. maxAnchorEnd = dimension == Dimension.Width
  309. ? v.X.GetAnchor (maxCalculatedSize + v.Frame.Width)
  310. : v.Y.GetAnchor (maxCalculatedSize + v.Frame.Height);
  311. }
  312. maxCalculatedSize = Math.Max (maxCalculatedSize, maxAnchorEnd);
  313. #endregion Anchored
  314. #region PosView
  315. // [x] PosView - Position is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
  316. List<View> posViewSubViews;
  317. if (dimension == Dimension.Width)
  318. {
  319. posViewSubViews = includedSubviews.Where (v => v.X.Has (typeof (PosView), out _)).ToList ();
  320. }
  321. else
  322. {
  323. posViewSubViews = includedSubviews.Where (v => v.Y.Has (typeof (PosView), out _)).ToList ();
  324. }
  325. for (var i = 0; i < posViewSubViews.Count; i++)
  326. {
  327. View v = posViewSubViews [i];
  328. // BUGBUG: The order may not be correct. May need to call TopologicalSort?
  329. // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL).
  330. if (dimension == Dimension.Width)
  331. {
  332. v.SetRelativeLayout (new (maxCalculatedSize, 0));
  333. }
  334. else
  335. {
  336. v.SetRelativeLayout (new (0, maxCalculatedSize));
  337. }
  338. int maxPosView = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height;
  339. if (maxPosView > maxCalculatedSize)
  340. {
  341. maxCalculatedSize = maxPosView;
  342. }
  343. }
  344. #endregion PosView
  345. // [x] PosCombine - Position is dependent if `Pos.Has ([one of the above]` - it can cause a change in `us.ContentSize`
  346. #region DimView
  347. // [x] DimView - Dimension is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
  348. List<View> dimViewSubViews;
  349. if (dimension == Dimension.Width)
  350. {
  351. dimViewSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has (typeof (DimView), out _)).ToList ();
  352. }
  353. else
  354. {
  355. dimViewSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has (typeof (DimView), out _)).ToList ();
  356. }
  357. for (var i = 0; i < dimViewSubViews.Count; i++)
  358. {
  359. View v = dimViewSubViews [i];
  360. // BUGBUG: The order may not be correct. May need to call TopologicalSort?
  361. // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL).
  362. if (dimension == Dimension.Width)
  363. {
  364. v.SetRelativeLayout (new (maxCalculatedSize, 0));
  365. }
  366. else
  367. {
  368. v.SetRelativeLayout (new (0, maxCalculatedSize));
  369. }
  370. int maxDimView = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height;
  371. if (maxDimView > maxCalculatedSize)
  372. {
  373. maxCalculatedSize = maxDimView;
  374. }
  375. }
  376. #endregion DimView
  377. }
  378. }
  379. // All sizes here are content-relative; ignoring adornments.
  380. // We take the largest of text and content.
  381. int max = int.Max (textSize, maxCalculatedSize);
  382. // And, if min: is set, it wins if larger
  383. max = int.Max (max, autoMin);
  384. // And, if max: is set, it wins if smaller
  385. max = int.Min (max, autoMax);
  386. Thickness thickness = us.GetAdornmentsThickness ();
  387. int adornmentThickness = dimension switch
  388. {
  389. Dimension.Width => thickness.Horizontal,
  390. Dimension.Height => thickness.Vertical,
  391. Dimension.None => 0,
  392. _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null)
  393. };
  394. max += adornmentThickness;
  395. return max;
  396. }
  397. }