View.Layout.cs 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239
  1. #nullable enable
  2. using System.Diagnostics;
  3. using Microsoft.CodeAnalysis;
  4. using static Unix.Terminal.Curses;
  5. namespace Terminal.Gui;
  6. public partial class View // Layout APIs
  7. {
  8. #region Frame/Position/Dimension
  9. /// <summary>
  10. /// Indicates whether the specified SuperView-relative coordinates are within the View's <see cref="Frame"/>.
  11. /// </summary>
  12. /// <param name="location">SuperView-relative coordinate</param>
  13. /// <returns><see langword="true"/> if the specified SuperView-relative coordinates are within the View.</returns>
  14. public virtual bool Contains (in Point location) { return Frame.Contains (location); }
  15. private Rectangle? _frame;
  16. /// <summary>Gets or sets the absolute location and dimension of the view.</summary>
  17. /// <value>
  18. /// The rectangle describing absolute location and dimension of the view, in coordinates relative to the
  19. /// <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>.
  20. /// </value>
  21. /// <remarks>
  22. /// <para>
  23. /// See the View Layout Deep Dive for more information:
  24. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  25. /// </para>
  26. /// <para>
  27. /// Frame is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>
  28. /// .
  29. /// </para>
  30. /// <para>
  31. /// Setting Frame will set <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/> to absoulte values.
  32. /// </para>
  33. /// <para>
  34. /// Changing this property will result in <see cref="NeedsLayout"/> and <see cref="NeedsDraw"/> to be set, resulting in the
  35. /// view being laid out and redrawn as appropriate in the next iteration of the <see cref="MainLoop"/>.
  36. /// </para>
  37. /// </remarks>
  38. public Rectangle Frame
  39. {
  40. get
  41. {
  42. if (_needsLayout)
  43. {
  44. //Debug.WriteLine("Frame_get with _layoutNeeded");
  45. }
  46. return _frame ?? Rectangle.Empty;
  47. }
  48. set
  49. {
  50. // This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged
  51. if (SetFrame (value with { Width = Math.Max (value.Width, 0), Height = Math.Max (value.Height, 0) }))
  52. {
  53. // If Frame gets set, set all Pos/Dim to Absolute values.
  54. _x = _frame!.Value.X;
  55. _y = _frame!.Value.Y;
  56. _width = _frame!.Value.Width;
  57. _height = _frame!.Value.Height;
  58. // Implicit layout is ok here because we are setting the Frame directly.
  59. Layout ();
  60. }
  61. }
  62. }
  63. /// <summary>
  64. /// INTERNAL API - Sets _frame, calls SetsNeedsLayout, and raises OnViewportChanged/ViewportChanged
  65. /// </summary>
  66. /// <param name="frame"></param>
  67. /// <returns><see langword="true"/> if the frame was changed.</returns>
  68. private bool SetFrame (in Rectangle frame)
  69. {
  70. if (_frame == frame)
  71. {
  72. return false;
  73. }
  74. var oldViewport = Rectangle.Empty;
  75. if (IsInitialized)
  76. {
  77. oldViewport = Viewport;
  78. }
  79. // This is the only place where _frame should be set directly. Use Frame = or SetFrame instead.
  80. _frame = frame;
  81. SetAdornmentFrames ();
  82. SetNeedsDraw ();
  83. SetNeedsLayout ();
  84. // BUGBUG: When SetFrame is called from Frame_set, this event gets raised BEFORE OnResizeNeeded. Is that OK?
  85. OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport));
  86. return true;
  87. }
  88. /// <summary>Gets the <see cref="Frame"/> with a screen-relative location.</summary>
  89. /// <returns>The location and size of the view in screen-relative coordinates.</returns>
  90. public virtual Rectangle FrameToScreen ()
  91. {
  92. Rectangle screen = Frame;
  93. View? current = SuperView;
  94. while (current is { })
  95. {
  96. if (current is Adornment adornment)
  97. {
  98. // Adornments don't have SuperViews; use Adornment.FrameToScreen override
  99. // which will give us the screen coordinates of the parent
  100. Rectangle parentScreen = adornment.FrameToScreen ();
  101. // Now add our Frame location
  102. parentScreen.Offset (screen.X, screen.Y);
  103. return parentScreen with { Size = Frame.Size };
  104. }
  105. Point viewportOffset = current.GetViewportOffsetFromFrame ();
  106. viewportOffset.Offset (current.Frame.X - current.Viewport.X, current.Frame.Y - current.Viewport.Y);
  107. screen.X += viewportOffset.X;
  108. screen.Y += viewportOffset.Y;
  109. current = current.SuperView;
  110. }
  111. return screen;
  112. }
  113. /// <summary>
  114. /// Converts a screen-relative coordinate to a Frame-relative coordinate. Frame-relative means relative to the
  115. /// View's <see cref="SuperView"/>'s <see cref="Viewport"/>.
  116. /// </summary>
  117. /// <returns>The coordinate relative to the <see cref="SuperView"/>'s <see cref="Viewport"/>.</returns>
  118. /// <param name="location">Screen-relative coordinate.</param>
  119. public virtual Point ScreenToFrame (in Point location)
  120. {
  121. if (SuperView is null)
  122. {
  123. return new (location.X - Frame.X, location.Y - Frame.Y);
  124. }
  125. Point superViewViewportOffset = SuperView.GetViewportOffsetFromFrame ();
  126. superViewViewportOffset.Offset (-SuperView.Viewport.X, -SuperView.Viewport.Y);
  127. Point frame = location;
  128. frame.Offset (-superViewViewportOffset.X, -superViewViewportOffset.Y);
  129. frame = SuperView.ScreenToFrame (frame);
  130. frame.Offset (-Frame.X, -Frame.Y);
  131. return frame;
  132. }
  133. // helper for X, Y, Width, Height setters to ensure consistency
  134. private void PosDimSet ()
  135. {
  136. SetNeedsLayout ();
  137. if (_x is PosAbsolute && _y is PosAbsolute && _width is DimAbsolute && _height is DimAbsolute)
  138. {
  139. // Implicit layout is ok here because all Pos/Dim are Absolute values.
  140. Layout ();
  141. if (SuperView is { } || this is Adornment { Parent: null })
  142. {
  143. // Ensure the next Application iteration tries to layout again
  144. SetNeedsLayout ();
  145. }
  146. }
  147. }
  148. private Pos _x = Pos.Absolute (0);
  149. /// <summary>Gets or sets the X position for the view (the column).</summary>
  150. /// <value>The <see cref="Pos"/> object representing the X position.</value>
  151. /// <remarks>
  152. /// <para>
  153. /// See the View Layout Deep Dive for more information:
  154. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  155. /// </para>
  156. /// <para>
  157. /// The position is relative to the <see cref="SuperView"/>'s Content, which is bound by
  158. /// <see cref="GetContentSize ()"/>.
  159. /// </para>
  160. /// <para>
  161. /// If set to a relative value (e.g. <see cref="Pos.Center"/>) the value is indeterminate until the view has been
  162. /// laid out (e.g. <see cref="Layout(System.Drawing.Size)"/> has been called).
  163. /// </para>
  164. /// <para>
  165. /// Changing this property will result in <see cref="NeedsLayout"/> and <see cref="NeedsDraw"/> to be set, resulting in the
  166. /// view being laid out and redrawn as appropriate in the next iteration of the <see cref="MainLoop"/>.
  167. /// </para>
  168. /// <para>
  169. /// Changing this property will cause <see cref="Frame"/> to be updated.
  170. /// </para>
  171. /// <para>The default value is <c>Pos.At (0)</c>.</para>
  172. /// </remarks>
  173. public Pos X
  174. {
  175. get => VerifyIsInitialized (_x, nameof (X));
  176. set
  177. {
  178. if (Equals (_x, value))
  179. {
  180. return;
  181. }
  182. _x = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (X)} cannot be null");
  183. PosDimSet ();
  184. }
  185. }
  186. private Pos _y = Pos.Absolute (0);
  187. /// <summary>Gets or sets the Y position for the view (the row).</summary>
  188. /// <value>The <see cref="Pos"/> object representing the Y position.</value>
  189. /// <remarks>
  190. /// <para>
  191. /// See the View Layout Deep Dive for more information:
  192. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  193. /// </para>
  194. /// <para>
  195. /// The position is relative to the <see cref="SuperView"/>'s Content, which is bound by
  196. /// <see cref="GetContentSize ()"/>.
  197. /// </para>
  198. /// <para>
  199. /// If set to a relative value (e.g. <see cref="Pos.Center"/>) the value is indeterminate until the view has been
  200. /// laid out (e.g. <see cref="Layout(System.Drawing.Size)"/> has been called).
  201. /// </para>
  202. /// <para>
  203. /// Changing this property will result in <see cref="NeedsLayout"/> and <see cref="NeedsDraw"/> to be set, resulting in the
  204. /// view being laid out and redrawn as appropriate in the next iteration of the <see cref="MainLoop"/>.
  205. /// </para>
  206. /// <para>
  207. /// Changing this property will cause <see cref="Frame"/> to be updated.
  208. /// </para>
  209. /// <para>The default value is <c>Pos.At (0)</c>.</para>
  210. /// </remarks>
  211. public Pos Y
  212. {
  213. get => VerifyIsInitialized (_y, nameof (Y));
  214. set
  215. {
  216. if (Equals (_y, value))
  217. {
  218. return;
  219. }
  220. _y = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Y)} cannot be null");
  221. PosDimSet ();
  222. }
  223. }
  224. private Dim? _height = Dim.Absolute (0);
  225. /// <summary>Gets or sets the height dimension of the view.</summary>
  226. /// <value>The <see cref="Dim"/> object representing the height of the view (the number of rows).</value>
  227. /// <remarks>
  228. /// <para>
  229. /// See the View Layout Deep Dive for more information:
  230. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  231. /// </para>
  232. /// <para>
  233. /// The dimension is relative to the <see cref="SuperView"/>'s Content, which is bound by
  234. /// <see cref="GetContentSize ()"/> .
  235. /// </para>
  236. /// <para>
  237. /// If set to a relative value (e.g. <see cref="DimFill"/>) the value is indeterminate until the view has been
  238. /// laid out (e.g. <see cref="Layout(System.Drawing.Size)"/> has been called).
  239. /// </para>
  240. /// <para>
  241. /// Changing this property will result in <see cref="NeedsLayout"/> and <see cref="NeedsDraw"/> to be set, resulting in the
  242. /// view being laid out and redrawn as appropriate in the next iteration of the <see cref="MainLoop"/>.
  243. /// </para>
  244. /// <para>
  245. /// Changing this property will cause <see cref="Frame"/> to be updated.
  246. /// </para>
  247. /// <para>The default value is <c>Dim.Sized (0)</c>.</para>
  248. /// </remarks>
  249. public Dim? Height
  250. {
  251. get => VerifyIsInitialized (_height, nameof (Height));
  252. set
  253. {
  254. if (Equals (_height, value))
  255. {
  256. return;
  257. }
  258. _height = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Height)} cannot be null");
  259. // Reset TextFormatter - Will be recalculated in SetTextFormatterSize
  260. TextFormatter.ConstrainToHeight = null;
  261. PosDimSet ();
  262. }
  263. }
  264. private Dim? _width = Dim.Absolute (0);
  265. /// <summary>Gets or sets the width dimension of the view.</summary>
  266. /// <value>The <see cref="Dim"/> object representing the width of the view (the number of columns).</value>
  267. /// <remarks>
  268. /// <para>
  269. /// See the View Layout Deep Dive for more information:
  270. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  271. /// </para>
  272. /// <para>
  273. /// The dimension is relative to the <see cref="SuperView"/>'s Content, which is bound by
  274. /// <see cref="GetContentSize ()"/>
  275. /// .
  276. /// </para>
  277. /// <para>
  278. /// If set to a relative value (e.g. <see cref="DimFill"/>) the value is indeterminate until the view has been
  279. /// laid out (e.g. <see cref="Layout(System.Drawing.Size)"/> has been called).
  280. /// </para>
  281. /// <para>
  282. /// Changing this property will result in <see cref="NeedsLayout"/> and <see cref="NeedsDraw"/> to be set, resulting in the
  283. /// view being laid out and redrawn as appropriate in the next iteration of the <see cref="MainLoop"/>.
  284. /// </para>
  285. /// <para>
  286. /// Changing this property will cause <see cref="Frame"/> to be updated.
  287. /// </para>
  288. /// <para>The default value is <c>Dim.Sized (0)</c>.</para>
  289. /// </remarks>
  290. public Dim? Width
  291. {
  292. get => VerifyIsInitialized (_width, nameof (Width));
  293. set
  294. {
  295. if (Equals (_width, value))
  296. {
  297. return;
  298. }
  299. _width = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Width)} cannot be null");
  300. // Reset TextFormatter - Will be recalculated in SetTextFormatterSize
  301. TextFormatter.ConstrainToWidth = null;
  302. PosDimSet ();
  303. }
  304. }
  305. #endregion Frame/Position/Dimension
  306. #region Core Layout API
  307. /// <summary>
  308. /// INTERNAL API - Performs layout of the specified views within the specified content size. Called by the Application main loop.
  309. /// </summary>
  310. /// <param name="views">The views to layout.</param>
  311. /// <param name="contentSize">The size to bound the views by.</param>
  312. /// <returns><see langword="true"/>If any of the views needed to be laid out.</returns>
  313. internal static bool Layout (IEnumerable<View> views, Size contentSize)
  314. {
  315. bool neededLayout = false;
  316. foreach (View v in views)
  317. {
  318. if (v.NeedsLayout)
  319. {
  320. neededLayout = true;
  321. v.Layout (contentSize);
  322. }
  323. }
  324. return neededLayout;
  325. }
  326. /// <summary>
  327. /// Performs layout of the view and its subviews within the specified content size.
  328. /// </summary>
  329. /// <remarks>
  330. /// <para>
  331. /// See the View Layout Deep Dive for more information:
  332. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  333. /// </para>
  334. /// <para>
  335. /// This method is intended to be called by the layout engine to
  336. /// prepare the view for layout and is exposed as a public API primarily for testing purposes.
  337. /// </para>
  338. /// </remarks>
  339. /// <param name="contentSize"></param>
  340. /// <returns><see langword="false"/>If the view could not be laid out (typically because a dependencies was not ready). </returns>
  341. public bool Layout (Size contentSize)
  342. {
  343. if (SetRelativeLayout (contentSize))
  344. {
  345. LayoutSubviews ();
  346. // Debug.Assert(!NeedsLayout);
  347. return true;
  348. }
  349. return false;
  350. }
  351. /// <summary>
  352. /// Performs layout of the view and its subviews using the content size of either the <see cref="SuperView"/> or <see cref="Application.Screen"/>.
  353. /// </summary>
  354. /// <remarks>
  355. /// <para>
  356. /// See the View Layout Deep Dive for more information:
  357. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  358. /// </para>
  359. /// <para>
  360. /// This method is intended to be called by the layout engine to
  361. /// prepare the view for layout and is exposed as a public API primarily for testing purposes.
  362. /// </para>
  363. /// </remarks>
  364. /// <returns><see langword="false"/>If the view could not be laid out (typically because dependency was not ready). </returns>
  365. public bool Layout ()
  366. {
  367. return Layout (GetContainerSize ());
  368. }
  369. /// <summary>
  370. /// Sets the position and size of this view, relative to the SuperView's ContentSize (nominally the same as
  371. /// <c>this.SuperView.GetContentSize ()</c>) based on the values of <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>,
  372. /// and <see cref="Height"/>.
  373. /// </summary>
  374. /// <remarks>
  375. /// <para>
  376. /// If <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, or <see cref="Height"/> are
  377. /// absolute, they will be updated to reflect the new size and position of the view. Otherwise, they
  378. /// are left unchanged.
  379. /// </para>
  380. /// <para>
  381. /// This method does not arrange subviews or adornments. It is intended to be called by the layout engine to
  382. /// prepare the view for layout and is exposed as a public API primarily for testing purposes.
  383. /// </para>
  384. /// <para>
  385. /// Some subviews may have SetRelativeLayout called on them as a side effect, particularly in DimAuto scenarios.
  386. /// </para>
  387. /// </remarks>
  388. /// <param name="superviewContentSize">
  389. /// The size of the SuperView's content (nominally the same as <c>this.SuperView.GetContentSize ()</c>).
  390. /// </param>
  391. /// <returns><see langword="true"/> if successful. <see langword="false"/> means a dependent View still needs layout.</returns>
  392. public bool SetRelativeLayout (Size superviewContentSize)
  393. {
  394. Debug.Assert (_x is { });
  395. Debug.Assert (_y is { });
  396. Debug.Assert (_width is { });
  397. Debug.Assert (_height is { });
  398. CheckDimAuto ();
  399. // TODO: Should move to View.LayoutSubviews?
  400. SetTextFormatterSize ();
  401. int newX, newW, newY, newH;
  402. try
  403. {
  404. // Calculate the new X, Y, Width, and Height
  405. // If the Width or Height is Dim.Auto, calculate the Width or Height first. Otherwise, calculate the X or Y first.
  406. if (_width.Has<DimAuto> (out _))
  407. {
  408. newW = _width.Calculate (0, superviewContentSize.Width, this, Dimension.Width);
  409. newX = _x.Calculate (superviewContentSize.Width, newW, this, Dimension.Width);
  410. if (newW != Frame.Width)
  411. {
  412. // Pos.Calculate gave us a new position. We need to redo dimension
  413. newW = _width.Calculate (newX, superviewContentSize.Width, this, Dimension.Width);
  414. }
  415. }
  416. else
  417. {
  418. newX = _x.Calculate (superviewContentSize.Width, _width, this, Dimension.Width);
  419. newW = _width.Calculate (newX, superviewContentSize.Width, this, Dimension.Width);
  420. }
  421. if (_height.Has<DimAuto> (out _))
  422. {
  423. newH = _height.Calculate (0, superviewContentSize.Height, this, Dimension.Height);
  424. newY = _y.Calculate (superviewContentSize.Height, newH, this, Dimension.Height);
  425. if (newH != Frame.Height)
  426. {
  427. // Pos.Calculate gave us a new position. We need to redo dimension
  428. newH = _height.Calculate (newY, superviewContentSize.Height, this, Dimension.Height);
  429. }
  430. }
  431. else
  432. {
  433. newY = _y.Calculate (superviewContentSize.Height, _height, this, Dimension.Height);
  434. newH = _height.Calculate (newY, superviewContentSize.Height, this, Dimension.Height);
  435. }
  436. }
  437. catch (LayoutException le)
  438. {
  439. //Debug.WriteLine ($"A Dim/PosFunc function threw (typically this is because a dependent View was not laid out)\n{le}.");
  440. return false;
  441. }
  442. Rectangle newFrame = new (newX, newY, newW, newH);
  443. if (Frame != newFrame)
  444. {
  445. // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height
  446. // This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged
  447. SetFrame (newFrame);
  448. if (_x is PosAbsolute)
  449. {
  450. _x = Frame.X;
  451. }
  452. if (_y is PosAbsolute)
  453. {
  454. _y = Frame.Y;
  455. }
  456. if (_width is DimAbsolute)
  457. {
  458. _width = Frame.Width;
  459. }
  460. if (_height is DimAbsolute)
  461. {
  462. _height = Frame.Height;
  463. }
  464. if (!string.IsNullOrEmpty (Title))
  465. {
  466. SetTitleTextFormatterSize ();
  467. }
  468. SuperView?.SetNeedsDraw ();
  469. }
  470. if (TextFormatter.ConstrainToWidth is null)
  471. {
  472. TextFormatter.ConstrainToWidth = GetContentSize ().Width;
  473. }
  474. if (TextFormatter.ConstrainToHeight is null)
  475. {
  476. TextFormatter.ConstrainToHeight = GetContentSize ().Height;
  477. }
  478. return true;
  479. }
  480. /// <summary>
  481. /// INTERNAL API - Causes the view's subviews and adornments to be laid out within the view's content areas. Assumes the view's relative layout has been set via <see cref="SetRelativeLayout"/>.
  482. /// </summary>
  483. /// <remarks>
  484. /// <para>
  485. /// See the View Layout Deep Dive for more information:
  486. /// <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
  487. /// </para>
  488. /// <para>
  489. /// The position and dimensions of the view are indeterminate until the view has been initialized. Therefore, the
  490. /// behavior of this method is indeterminate if <see cref="IsInitialized"/> is <see langword="false"/>.
  491. /// </para>
  492. /// <para>Raises the <see cref="SubviewsLaidOut"/> event before it returns.</para>
  493. /// </remarks>
  494. internal void LayoutSubviews ()
  495. {
  496. if (!NeedsLayout)
  497. {
  498. return;
  499. }
  500. CheckDimAuto ();
  501. Size contentSize = GetContentSize ();
  502. OnSubviewLayout (new (contentSize));
  503. SubviewLayout?.Invoke (this, new (contentSize));
  504. // The Adornments already have their Frame's set by SetRelativeLayout so we call LayoutSubViews vs. Layout here.
  505. if (Margin is { Subviews.Count: > 0 })
  506. {
  507. Margin.LayoutSubviews ();
  508. }
  509. if (Border is { Subviews.Count: > 0 })
  510. {
  511. Border.LayoutSubviews ();
  512. }
  513. if (Padding is { Subviews.Count: > 0 })
  514. {
  515. Padding.LayoutSubviews ();
  516. }
  517. // Sort out the dependencies of the X, Y, Width, Height properties
  518. HashSet<View> nodes = new ();
  519. HashSet<(View, View)> edges = new ();
  520. CollectAll (this, ref nodes, ref edges);
  521. List<View> ordered = TopologicalSort (SuperView!, nodes, edges);
  522. List<View> redo = new ();
  523. foreach (View v in ordered)
  524. {
  525. if (!v.Layout (contentSize))
  526. {
  527. redo.Add (v);
  528. }
  529. }
  530. bool layoutStillNeeded = false;
  531. if (redo.Count > 0)
  532. {
  533. foreach (View v in ordered)
  534. {
  535. if (!v.Layout (contentSize))
  536. {
  537. layoutStillNeeded = true;
  538. }
  539. }
  540. }
  541. // If the 'to' is rooted to 'from' it's a special-case.
  542. // Use Layout with the ContentSize of the 'from'.
  543. // See the Nested_SubViews_Ref_Topmost_SuperView unit test
  544. if (edges.Count > 0 && GetTopSuperView () is { })
  545. {
  546. foreach ((View from, View to) in edges)
  547. {
  548. // QUESTION: Do we test this with adornments well enough?
  549. to.Layout (from.GetContentSize ());
  550. }
  551. }
  552. _needsLayout = layoutStillNeeded;
  553. OnSubviewsLaidOut (new (contentSize));
  554. SubviewsLaidOut?.Invoke (this, new (contentSize));
  555. }
  556. /// <summary>
  557. /// Called from <see cref="LayoutSubviews"/> before any subviews
  558. /// have been laid out.
  559. /// </summary>
  560. /// <remarks>
  561. /// Override to perform tasks when the layout is changing.
  562. /// </remarks>
  563. protected virtual void OnSubviewLayout (LayoutEventArgs args) { }
  564. /// <summary>Raised by <see cref="LayoutSubviews"/> before any subviews
  565. /// have been laid out.</summary>
  566. /// <remarks>
  567. /// Subscribe to this event to perform tasks when the layout is changing.
  568. /// </remarks>
  569. public event EventHandler<LayoutEventArgs>? SubviewLayout;
  570. /// <summary>
  571. /// Called from <see cref="LayoutSubviews"/> after all sub-views
  572. /// have been laid out.
  573. /// </summary>
  574. /// <remarks>
  575. /// Override to perform tasks after the <see cref="View"/> has been resized or the layout has
  576. /// otherwise changed.
  577. /// </remarks>
  578. protected virtual void OnSubviewsLaidOut (LayoutEventArgs args) { }
  579. /// <summary>Raised after all sub-views have been laid out.</summary>
  580. /// <remarks>
  581. /// Subscribe to this event to perform tasks after the <see cref="View"/> has been resized or the layout has
  582. /// otherwise changed.
  583. /// </remarks>
  584. public event EventHandler<LayoutEventArgs>? SubviewsLaidOut;
  585. #endregion Core Layout API
  586. #region NeedsLayout
  587. // We expose no setter for this to ensure that the ONLY place it's changed is in SetNeedsLayout
  588. private bool _needsLayout = true;
  589. /// <summary>
  590. /// Indicates the View's Frame or the layout of the View's subviews (including Adornments) have
  591. /// changed since the last time the View was laid out.
  592. /// </summary>
  593. /// <remarks>
  594. /// <para>Used to prevent <see cref="Layout()"/> from needlessly computing
  595. /// layout.
  596. /// </para>
  597. /// </remarks>
  598. /// <value>
  599. /// <see langword="true"/> if layout is needed.
  600. /// </value>
  601. public bool NeedsLayout => _needsLayout;
  602. /// <summary>
  603. /// Sets <see cref="NeedsLayout"/> to return <see langword="true"/>, indicating this View and all of it's subviews (including adornments) need to be laid out in the next Application iteration.
  604. /// </summary>
  605. /// <remarks>
  606. /// <para>
  607. /// The <see cref="MainLoop"/> will cause <see cref="Layout()"/> to be called on the next <see cref="Application.Iteration"/> so there is normally no reason to call see <see cref="Layout()"/>.
  608. /// </para>
  609. /// </remarks>
  610. public void SetNeedsLayout ()
  611. {
  612. _needsLayout = true;
  613. if (Margin is { Subviews.Count: > 0 })
  614. {
  615. Margin.SetNeedsLayout ();
  616. }
  617. if (Border is { Subviews.Count: > 0 })
  618. {
  619. Border.SetNeedsLayout ();
  620. }
  621. if (Padding is { Subviews.Count: > 0 })
  622. {
  623. Padding.SetNeedsLayout ();
  624. }
  625. // Use a stack to avoid recursion
  626. Stack<View> stack = new Stack<View> (Subviews);
  627. while (stack.Count > 0)
  628. {
  629. View current = stack.Pop ();
  630. if (!current.NeedsLayout)
  631. {
  632. current._needsLayout = true;
  633. if (current.Margin is { Subviews.Count: > 0 })
  634. {
  635. current.Margin.SetNeedsLayout ();
  636. }
  637. if (current.Border is { Subviews.Count: > 0 })
  638. {
  639. current.Border.SetNeedsLayout ();
  640. }
  641. if (current.Padding is { Subviews.Count: > 0 })
  642. {
  643. current.Padding.SetNeedsLayout ();
  644. }
  645. foreach (View subview in current.Subviews)
  646. {
  647. stack.Push (subview);
  648. }
  649. }
  650. }
  651. TextFormatter.NeedsFormat = true;
  652. if (SuperView is { NeedsLayout: false })
  653. {
  654. SuperView?.SetNeedsLayout ();
  655. }
  656. if (SuperView is null)
  657. {
  658. foreach (var tl in Application.TopLevels)
  659. {
  660. tl.SetNeedsDraw ();
  661. }
  662. }
  663. if (this is not Adornment adornment)
  664. {
  665. return;
  666. }
  667. if (adornment.Parent is { NeedsLayout: false })
  668. {
  669. adornment.Parent?.SetNeedsLayout ();
  670. }
  671. }
  672. #endregion NeedsLayout
  673. #region Topological Sort
  674. /// <summary>
  675. /// INTERNAL API - Collects all views and their dependencies from a given starting view for layout purposes. Used by
  676. /// <see cref="TopologicalSort"/> to create an ordered list of views to layout.
  677. /// </summary>
  678. /// <param name="from">The starting view from which to collect dependencies.</param>
  679. /// <param name="nNodes">A reference to a set of views representing nodes in the layout graph.</param>
  680. /// <param name="nEdges">
  681. /// A reference to a set of tuples representing edges in the layout graph, where each tuple consists of a pair of views
  682. /// indicating a dependency.
  683. /// </param>
  684. internal void CollectAll (View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
  685. {
  686. foreach (View? v in from.InternalSubviews)
  687. {
  688. nNodes.Add (v);
  689. CollectPos (v.X, v, ref nNodes, ref nEdges);
  690. CollectPos (v.Y, v, ref nNodes, ref nEdges);
  691. CollectDim (v.Width, v, ref nNodes, ref nEdges);
  692. CollectDim (v.Height, v, ref nNodes, ref nEdges);
  693. }
  694. }
  695. /// <summary>
  696. /// INTERNAL API - Collects dimension (where Width or Height is `DimView`) dependencies for a given view.
  697. /// </summary>
  698. /// <param name="dim">The dimension (width or height) to collect dependencies for.</param>
  699. /// <param name="from">The view for which to collect dimension dependencies.</param>
  700. /// <param name="nNodes">A reference to a set of views representing nodes in the layout graph.</param>
  701. /// <param name="nEdges">
  702. /// A reference to a set of tuples representing edges in the layout graph, where each tuple consists of a pair of views
  703. /// indicating a dependency.
  704. /// </param>
  705. internal void CollectDim (Dim? dim, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
  706. {
  707. if (dim!.Has<DimView> (out DimView dv))
  708. {
  709. if (dv.Target != this)
  710. {
  711. nEdges.Add ((dv.Target!, from));
  712. }
  713. }
  714. if (dim!.Has<DimCombine> (out DimCombine dc))
  715. {
  716. CollectDim (dc.Left, from, ref nNodes, ref nEdges);
  717. CollectDim (dc.Right, from, ref nNodes, ref nEdges);
  718. }
  719. }
  720. /// <summary>
  721. /// INTERNAL API - Collects position (where X or Y is `PosView`) dependencies for a given view.
  722. /// </summary>
  723. /// <param name="pos">The position (X or Y) to collect dependencies for.</param>
  724. /// <param name="from">The view for which to collect position dependencies.</param>
  725. /// <param name="nNodes">A reference to a set of views representing nodes in the layout graph.</param>
  726. /// <param name="nEdges">
  727. /// A reference to a set of tuples representing edges in the layout graph, where each tuple consists of a pair of views
  728. /// indicating a dependency.
  729. /// </param>
  730. internal void CollectPos (Pos pos, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
  731. {
  732. // TODO: Use Pos.Has<T> instead.
  733. switch (pos)
  734. {
  735. case PosView pv:
  736. Debug.Assert (pv.Target is { });
  737. if (pv.Target != this)
  738. {
  739. nEdges.Add ((pv.Target!, from));
  740. }
  741. return;
  742. case PosCombine pc:
  743. CollectPos (pc.Left, from, ref nNodes, ref nEdges);
  744. CollectPos (pc.Right, from, ref nNodes, ref nEdges);
  745. break;
  746. }
  747. }
  748. // https://en.wikipedia.org/wiki/Topological_sorting
  749. internal static List<View> TopologicalSort (
  750. View superView,
  751. IEnumerable<View> nodes,
  752. ICollection<(View From, View To)> edges
  753. )
  754. {
  755. List<View> result = new ();
  756. // Set of all nodes with no incoming edges
  757. HashSet<View> noEdgeNodes = new (nodes.Where (n => edges.All (e => !e.To.Equals (n))));
  758. while (noEdgeNodes.Any ())
  759. {
  760. // remove a node n from S
  761. View n = noEdgeNodes.First ();
  762. noEdgeNodes.Remove (n);
  763. // add n to tail of L
  764. if (n != superView)
  765. {
  766. result.Add (n);
  767. }
  768. // for each node m with an edge e from n to m do
  769. foreach ((View From, View To) e in edges.Where (e => e.From.Equals (n)).ToArray ())
  770. {
  771. View m = e.To;
  772. // remove edge e from the graph
  773. edges.Remove (e);
  774. // if m has no other incoming edges then
  775. if (edges.All (me => !me.To.Equals (m)) && m != superView)
  776. {
  777. // insert m into S
  778. noEdgeNodes.Add (m);
  779. }
  780. }
  781. }
  782. if (!edges.Any ())
  783. {
  784. return result;
  785. }
  786. foreach ((View from, View to) in edges)
  787. {
  788. if (from == to)
  789. {
  790. // if not yet added to the result, add it and remove from edge
  791. if (result.Find (v => v == from) is null)
  792. {
  793. result.Add (from);
  794. }
  795. edges.Remove ((from, to));
  796. }
  797. else if (from.SuperView == to.SuperView)
  798. {
  799. // if 'from' is not yet added to the result, add it
  800. if (result.Find (v => v == from) is null)
  801. {
  802. result.Add (from);
  803. }
  804. // if 'to' is not yet added to the result, add it
  805. if (result.Find (v => v == to) is null)
  806. {
  807. result.Add (to);
  808. }
  809. // remove from edge
  810. edges.Remove ((from, to));
  811. }
  812. else if (from != superView?.GetTopSuperView (to, from) && !ReferenceEquals (from, to))
  813. {
  814. if (ReferenceEquals (from.SuperView, to))
  815. {
  816. throw new LayoutException (
  817. $"ComputedLayout for \"{superView}\": \"{to}\" "
  818. + $"references a SubView (\"{from}\")."
  819. );
  820. }
  821. throw new LayoutException (
  822. $"ComputedLayout for \"{superView}\": \"{from}\" "
  823. + $"linked with \"{to}\" was not found. Did you forget to add it to {superView}?"
  824. );
  825. }
  826. }
  827. // return L (a topologically sorted order)
  828. return result;
  829. } // TopologicalSort
  830. #endregion Topological Sort
  831. #region Utilities
  832. /// <summary>
  833. /// INTERNAL API - Gets the size of the SuperView's content (nominally the same as
  834. /// the SuperView's <see cref="GetContentSize ()"/>) or the screen size if there's no SuperView.
  835. /// </summary>
  836. /// <returns></returns>
  837. private Size GetContainerSize ()
  838. {
  839. // TODO: Get rid of refs to Top
  840. Size superViewContentSize = SuperView?.GetContentSize () ??
  841. (Application.Top is { } && Application.Top != this && Application.Top.IsInitialized
  842. ? Application.Top.GetContentSize ()
  843. : Application.Screen.Size);
  844. return superViewContentSize;
  845. }
  846. // BUGBUG: This method interferes with Dialog/MessageBox default min/max size.
  847. // TODO: Get rid of MenuBar coupling as part of https://github.com/gui-cs/Terminal.Gui/issues/2975
  848. /// <summary>
  849. /// Gets a new location of the <see cref="View"/> that is within the Viewport of the <paramref name="viewToMove"/>'s
  850. /// <see cref="View.SuperView"/> (e.g. for dragging a Window). The `out` parameters are the new X and Y coordinates.
  851. /// </summary>
  852. /// <remarks>
  853. /// If <paramref name="viewToMove"/> does not have a <see cref="View.SuperView"/> or it's SuperView is not
  854. /// <see cref="Application.Top"/> the position will be bound by <see cref="Application.Screen"/>.
  855. /// </remarks>
  856. /// <param name="viewToMove">The View that is to be moved.</param>
  857. /// <param name="targetX">The target x location.</param>
  858. /// <param name="targetY">The target y location.</param>
  859. /// <param name="nx">The new x location that will ensure <paramref name="viewToMove"/> will be fully visible.</param>
  860. /// <param name="ny">The new y location that will ensure <paramref name="viewToMove"/> will be fully visible.</param>
  861. /// <returns>
  862. /// Either <see cref="Application.Top"/> (if <paramref name="viewToMove"/> does not have a Super View) or
  863. /// <paramref name="viewToMove"/>'s SuperView. This can be used to ensure LayoutSubviews is called on the correct View.
  864. /// </returns>
  865. internal static View? GetLocationEnsuringFullVisibility (
  866. View viewToMove,
  867. int targetX,
  868. int targetY,
  869. out int nx,
  870. out int ny
  871. )
  872. {
  873. int maxDimension;
  874. View? superView;
  875. if (viewToMove is not Toplevel || viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top)
  876. {
  877. maxDimension = Application.Screen.Width;
  878. superView = Application.Top;
  879. }
  880. else
  881. {
  882. // Use the SuperView's Viewport, not Frame
  883. maxDimension = viewToMove!.SuperView.Viewport.Width;
  884. superView = viewToMove.SuperView;
  885. }
  886. if (superView?.Margin is { } && superView == viewToMove!.SuperView)
  887. {
  888. maxDimension -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right;
  889. }
  890. if (viewToMove!.Frame.Width <= maxDimension)
  891. {
  892. nx = Math.Max (targetX, 0);
  893. nx = nx + viewToMove.Frame.Width > maxDimension ? Math.Max (maxDimension - viewToMove.Frame.Width, 0) : nx;
  894. if (nx > viewToMove.Frame.X + viewToMove.Frame.Width)
  895. {
  896. nx = Math.Max (viewToMove.Frame.Right, 0);
  897. }
  898. }
  899. else
  900. {
  901. nx = targetX;
  902. }
  903. //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}");
  904. var menuVisible = false;
  905. var statusVisible = false;
  906. if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top)
  907. {
  908. menuVisible = Application.Top?.MenuBar?.Visible == true;
  909. }
  910. else
  911. {
  912. View? t = viewToMove!.SuperView;
  913. while (t is { } and not Toplevel)
  914. {
  915. t = t.SuperView;
  916. }
  917. if (t is Toplevel topLevel)
  918. {
  919. menuVisible = topLevel.MenuBar?.Visible == true;
  920. }
  921. }
  922. if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top)
  923. {
  924. maxDimension = menuVisible ? 1 : 0;
  925. }
  926. else
  927. {
  928. maxDimension = 0;
  929. }
  930. ny = Math.Max (targetY, maxDimension);
  931. if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top)
  932. {
  933. maxDimension = statusVisible ? Application.Screen.Height - 1 : Application.Screen.Height;
  934. }
  935. else
  936. {
  937. maxDimension = statusVisible ? viewToMove!.SuperView.Viewport.Height - 1 : viewToMove!.SuperView.Viewport.Height;
  938. }
  939. if (superView?.Margin is { } && superView == viewToMove?.SuperView)
  940. {
  941. maxDimension -= superView.GetAdornmentsThickness ().Top + superView.GetAdornmentsThickness ().Bottom;
  942. }
  943. ny = Math.Min (ny, maxDimension);
  944. if (viewToMove?.Frame.Height <= maxDimension)
  945. {
  946. ny = ny + viewToMove.Frame.Height > maxDimension
  947. ? Math.Max (maxDimension - viewToMove.Frame.Height, menuVisible ? 1 : 0)
  948. : ny;
  949. if (ny > viewToMove.Frame.Y + viewToMove.Frame.Height)
  950. {
  951. ny = Math.Max (viewToMove.Frame.Bottom, 0);
  952. }
  953. }
  954. //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}");
  955. return superView!;
  956. }
  957. #endregion Utilities
  958. #region Diagnostics and Verification
  959. // Diagnostics to highlight when X or Y is read before the view has been initialized
  960. private Pos VerifyIsInitialized (Pos pos, string member)
  961. {
  962. //#if DEBUG
  963. // if (pos.ReferencesOtherViews () && !IsInitialized)
  964. // {
  965. // Debug.WriteLine (
  966. // $"WARNING: {member} = {pos} of {this} is dependent on other views and {member} "
  967. // + $"is being accessed before the View has been initialized. This is likely a bug."
  968. // );
  969. // }
  970. //#endif // DEBUG
  971. return pos;
  972. }
  973. // Diagnostics to highlight when Width or Height is read before the view has been initialized
  974. private Dim? VerifyIsInitialized (Dim? dim, string member)
  975. {
  976. //#if DEBUG
  977. // if (dim.ReferencesOtherViews () && !IsInitialized)
  978. // {
  979. // Debug.WriteLine (
  980. // $"WARNING: {member} = {dim} of {this} is dependent on other views and {member} "
  981. // + $"is being accessed before the View has been initialized. This is likely a bug."
  982. // );
  983. // }
  984. //#endif // DEBUG
  985. return dim;
  986. }
  987. /// <summary>Gets or sets whether validation of <see cref="Pos"/> and <see cref="Dim"/> occurs.</summary>
  988. /// <remarks>
  989. /// Setting this to <see langword="true"/> will enable validation of <see cref="X"/>, <see cref="Y"/>,
  990. /// <see cref="Width"/>, and <see cref="Height"/> during set operations and in <see cref="LayoutSubviews"/>. If invalid
  991. /// settings are discovered exceptions will be thrown indicating the error. This will impose a performance penalty and
  992. /// thus should only be used for debugging.
  993. /// </remarks>
  994. public bool ValidatePosDim { get; set; }
  995. // TODO: Move this logic into the Pos/Dim classes
  996. /// <summary>
  997. /// Throws an <see cref="InvalidOperationException"/> if any SubViews are using Dim objects that depend on this
  998. /// Views dimensions.
  999. /// </summary>
  1000. /// <exception cref="InvalidOperationException"></exception>
  1001. private void CheckDimAuto ()
  1002. {
  1003. if (!ValidatePosDim || !IsInitialized)
  1004. {
  1005. return;
  1006. }
  1007. var widthAuto = Width as DimAuto;
  1008. var heightAuto = Height as DimAuto;
  1009. // Verify none of the subviews are using Dim objects that depend on the SuperView's dimensions.
  1010. foreach (View view in Subviews)
  1011. {
  1012. if (widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Content) && ContentSizeTracksViewport)
  1013. {
  1014. ThrowInvalid (view, view.Width, nameof (view.Width));
  1015. ThrowInvalid (view, view.X, nameof (view.X));
  1016. }
  1017. if (heightAuto is { } && heightAuto.Style.FastHasFlags (DimAutoStyle.Content) && ContentSizeTracksViewport)
  1018. {
  1019. ThrowInvalid (view, view.Height, nameof (view.Height));
  1020. ThrowInvalid (view, view.Y, nameof (view.Y));
  1021. }
  1022. }
  1023. return;
  1024. void ThrowInvalid (View view, object? checkPosDim, string name)
  1025. {
  1026. object? bad = null;
  1027. switch (checkPosDim)
  1028. {
  1029. case Pos pos and PosAnchorEnd:
  1030. break;
  1031. case Pos pos and not PosAbsolute and not PosView and not PosCombine:
  1032. bad = pos;
  1033. break;
  1034. case Pos pos and PosCombine:
  1035. // Recursively check for not Absolute or not View
  1036. ThrowInvalid (view, (pos as PosCombine)?.Left, name);
  1037. ThrowInvalid (view, (pos as PosCombine)?.Right, name);
  1038. break;
  1039. case Dim dim and DimAuto:
  1040. break;
  1041. case Dim dim and DimFill:
  1042. break;
  1043. case Dim dim and not DimAbsolute and not DimView and not DimCombine:
  1044. bad = dim;
  1045. break;
  1046. case Dim dim and DimCombine:
  1047. // Recursively check for not Absolute or not View
  1048. ThrowInvalid (view, (dim as DimCombine)?.Left, name);
  1049. ThrowInvalid (view, (dim as DimCombine)?.Right, name);
  1050. break;
  1051. }
  1052. if (bad != null)
  1053. {
  1054. throw new LayoutException (
  1055. $"{view.GetType ().Name}.{name} = {bad.GetType ().Name} "
  1056. + $"which depends on the SuperView's dimensions and the SuperView uses Dim.Auto."
  1057. );
  1058. }
  1059. }
  1060. }
  1061. #endregion Diagnostics and Verification
  1062. }