ViewLayout.cs 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. namespace Terminal.Gui;
  7. /// <summary>
  8. /// <para>
  9. /// Indicates the LayoutStyle for the <see cref="View"/>.
  10. /// </para>
  11. /// <para>
  12. /// If Absolute, the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and
  13. /// <see cref="View.Height"/>
  14. /// objects are all absolute values and are not relative. The position and size of the view is described by
  15. /// <see cref="View.Frame"/>.
  16. /// </para>
  17. /// <para>
  18. /// If Computed, one or more of the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, or
  19. /// <see cref="View.Height"/>
  20. /// objects are relative to the <see cref="View.SuperView"/> and are computed at layout time.
  21. /// </para>
  22. /// </summary>
  23. public enum LayoutStyle {
  24. /// <summary>
  25. /// Indicates the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and <see cref="View.Height"/>
  26. /// objects are all absolute values and are not relative. The position and size of the view is described by
  27. /// <see cref="View.Frame"/>.
  28. /// </summary>
  29. Absolute,
  30. /// <summary>
  31. /// Indicates one or more of the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, or
  32. /// <see cref="View.Height"/>
  33. /// objects are relative to the <see cref="View.SuperView"/> and are computed at layout time. The position and size of the
  34. /// view
  35. /// will be computed based on these objects at layout time. <see cref="View.Frame"/> will provide the absolute computed
  36. /// values.
  37. /// </summary>
  38. Computed
  39. }
  40. public partial class View {
  41. bool _autoSize;
  42. Rect _frame;
  43. Dim _height = Dim.Sized (0);
  44. Dim _width = Dim.Sized (0);
  45. Pos _x = Pos.At (0);
  46. Pos _y = Pos.At (0);
  47. /// <summary>
  48. /// Gets or sets the absolute location and dimension of the view.
  49. /// </summary>
  50. /// <value>
  51. /// The rectangle describing absolute location and dimension of the view,
  52. /// in coordinates relative to the <see cref="SuperView"/>'s <see cref="Bounds"/>.
  53. /// </value>
  54. /// <remarks>
  55. /// <para>
  56. /// Frame is relative to the <see cref="SuperView"/>'s <see cref="Bounds"/>.
  57. /// </para>
  58. /// <para>
  59. /// Setting Frame will set <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/>
  60. /// to the values of the corresponding properties of the <paramref name="value"/> parameter.
  61. /// </para>
  62. /// <para>
  63. /// This causes <see cref="LayoutStyle"/> to be <see cref="LayoutStyle.Absolute"/>.
  64. /// </para>
  65. /// <para>
  66. /// Altering the Frame will eventually (when the view hierarchy is next laid out via see cref="LayoutSubviews"/>)
  67. /// cause <see cref="LayoutSubview(View, Rect)"/> and <see cref="OnDrawContent(Rect)"/> methods to be called.
  68. /// </para>
  69. /// </remarks>
  70. public Rect Frame {
  71. get => _frame;
  72. set {
  73. _frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0));
  74. // If Frame gets set, by definition, the View is now LayoutStyle.Absolute, so
  75. // set all Pos/Dim to Absolute values.
  76. _x = _frame.X;
  77. _y = _frame.Y;
  78. _width = _frame.Width;
  79. _height = _frame.Height;
  80. // TODO: Figure out if the below can be optimized.
  81. if (IsInitialized /*|| LayoutStyle == LayoutStyle.Absolute*/) {
  82. LayoutFrames ();
  83. TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
  84. SetNeedsLayout ();
  85. SetNeedsDisplay ();
  86. }
  87. }
  88. }
  89. /// <summary>
  90. /// The frame (specified as a <see cref="Thickness"/>) that separates a View from other SubViews of the same SuperView.
  91. /// The margin offsets the <see cref="Bounds"/> from the <see cref="Frame"/>.
  92. /// </summary>
  93. /// <remarks>
  94. /// <para>
  95. /// The frames (<see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/>) are not part of the View's
  96. /// content
  97. /// and are not clipped by the View's Clip Area.
  98. /// </para>
  99. /// <para>
  100. /// Changing the size of a frame (<see cref="Margin"/>, <see cref="Border"/>, or <see cref="Padding"/>)
  101. /// will change the size of the <see cref="Frame"/> and trigger <see cref="LayoutSubviews"/> to update the layout
  102. /// of the
  103. /// <see cref="SuperView"/> and its <see cref="Subviews"/>.
  104. /// </para>
  105. /// </remarks>
  106. public Frame Margin { get; private set; }
  107. /// <summary>
  108. /// The frame (specified as a <see cref="Thickness"/>) inside of the view that offsets the <see cref="Bounds"/> from the
  109. /// <see cref="Margin"/>.
  110. /// The Border provides the space for a visual border (drawn using line-drawing glyphs) and the Title.
  111. /// The Border expands inward; in other words if `Border.Thickness.Top == 2` the border and
  112. /// title will take up the first row and the second row will be filled with spaces.
  113. /// </summary>
  114. /// <remarks>
  115. /// <para>
  116. /// <see cref="BorderStyle"/> provides a simple helper for turning a simple border frame on or off.
  117. /// </para>
  118. /// <para>
  119. /// The frames (<see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/>) are not part of the View's
  120. /// content
  121. /// and are not clipped by the View's Clip Area.
  122. /// </para>
  123. /// <para>
  124. /// Changing the size of a frame (<see cref="Margin"/>, <see cref="Border"/>, or <see cref="Padding"/>)
  125. /// will change the size of the <see cref="Frame"/> and trigger <see cref="LayoutSubviews"/> to update the layout
  126. /// of the
  127. /// <see cref="SuperView"/> and its <see cref="Subviews"/>.
  128. /// </para>
  129. /// </remarks>
  130. public Frame Border { get; private set; }
  131. /// <summary>
  132. /// Gets or sets whether the view has a one row/col thick border.
  133. /// </summary>
  134. /// <remarks>
  135. /// <para>
  136. /// This is a helper for manipulating the view's <see cref="Border"/>. Setting this property to any value other
  137. /// than
  138. /// <see cref="LineStyle.None"/> is equivalent to setting <see cref="Border"/>'s <see cref="Frame.Thickness"/>
  139. /// to `1` and <see cref="BorderStyle"/> to the value.
  140. /// </para>
  141. /// <para>
  142. /// Setting this property to <see cref="LineStyle.None"/> is equivalent to setting <see cref="Border"/>'s
  143. /// <see cref="Frame.Thickness"/>
  144. /// to `0` and <see cref="BorderStyle"/> to <see cref="LineStyle.None"/>.
  145. /// </para>
  146. /// <para>
  147. /// For more advanced customization of the view's border, manipulate see <see cref="Border"/> directly.
  148. /// </para>
  149. /// </remarks>
  150. public LineStyle BorderStyle {
  151. get => Border?.BorderStyle ?? LineStyle.None;
  152. set {
  153. if (Border == null) {
  154. throw new InvalidOperationException ("Border is null; this is likely a bug.");
  155. }
  156. if (value != LineStyle.None) {
  157. Border.Thickness = new Thickness (1);
  158. } else {
  159. Border.Thickness = new Thickness (0);
  160. }
  161. Border.BorderStyle = value;
  162. LayoutFrames ();
  163. SetNeedsLayout ();
  164. }
  165. }
  166. /// <summary>
  167. /// The frame (specified as a <see cref="Thickness"/>) inside of the view that offsets the <see cref="Bounds"/> from the
  168. /// <see cref="Border"/>.
  169. /// </summary>
  170. /// <remarks>
  171. /// <para>
  172. /// The frames (<see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/>) are not part of the View's
  173. /// content
  174. /// and are not clipped by the View's Clip Area.
  175. /// </para>
  176. /// <para>
  177. /// Changing the size of a frame (<see cref="Margin"/>, <see cref="Border"/>, or <see cref="Padding"/>)
  178. /// will change the size of the <see cref="Frame"/> and trigger <see cref="LayoutSubviews"/> to update the layout
  179. /// of the
  180. /// <see cref="SuperView"/> and its <see cref="Subviews"/>.
  181. /// </para>
  182. /// </remarks>
  183. public Frame Padding { get; private set; }
  184. /// <summary>
  185. /// <para>
  186. /// Indicates the LayoutStyle for the <see cref="View"/>.
  187. /// </para>
  188. /// <para>
  189. /// If Absolute, the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and
  190. /// <see cref="View.Height"/>
  191. /// objects are all absolute values and are not relative. The position and size of the view is described by
  192. /// <see cref="View.Frame"/>.
  193. /// </para>
  194. /// <para>
  195. /// If Computed, one or more of the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, or
  196. /// <see cref="View.Height"/>
  197. /// objects are relative to the <see cref="View.SuperView"/> and are computed at layout time.
  198. /// </para>
  199. /// </summary>
  200. /// <value>The layout style.</value>
  201. public LayoutStyle LayoutStyle {
  202. get {
  203. if (_x is Pos.PosAbsolute && _y is Pos.PosAbsolute && _width is Dim.DimAbsolute && _height is Dim.DimAbsolute) {
  204. return LayoutStyle.Absolute;
  205. }
  206. return LayoutStyle.Computed;
  207. }
  208. }
  209. /// <summary>
  210. /// The bounds represent the View-relative rectangle used for this view; the area inside of the view where subviews and
  211. /// content are presented.
  212. /// </summary>
  213. /// <value>The rectangle describing the location and size of the area where the views' subviews and content are drawn.</value>
  214. /// <remarks>
  215. /// <para>
  216. /// If <see cref="LayoutStyle"/> is <see cref="LayoutStyle.Computed"/> the value of Bounds is indeterminate until
  217. /// the view has been initialized (<see creft="IsInitialized"/> is true) and <see cref="LayoutSubviews"/> has been
  218. /// called.
  219. /// </para>
  220. /// <para>
  221. /// Updates to the Bounds updates <see cref="Frame"/>, and has the same effect as updating the
  222. /// <see cref="Frame"/>.
  223. /// </para>
  224. /// <para>
  225. /// Altering the Bounds will eventually (when the view is next laid out) cause the
  226. /// <see cref="LayoutSubview(View, Rect)"/>
  227. /// and <see cref="OnDrawContent(Rect)"/> methods to be called.
  228. /// </para>
  229. /// <para>
  230. /// Because <see cref="Bounds"/> coordinates are relative to the upper-left corner of the <see cref="View"/>,
  231. /// the coordinates of the upper-left corner of the rectangle returned by this property are (0,0).
  232. /// Use this property to obtain the size of the area of the view for tasks such as drawing the view's contents.
  233. /// </para>
  234. /// </remarks>
  235. public virtual Rect Bounds {
  236. get {
  237. #if DEBUG
  238. if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) {
  239. Debug.WriteLine ($"WARNING: Bounds is being accessed before the View has been initialized. This is likely a bug in {this}");
  240. }
  241. #endif // DEBUG
  242. var frameRelativeBounds = FrameGetInsideBounds ();
  243. return new Rect (default, frameRelativeBounds.Size);
  244. }
  245. set {
  246. Frame = new Rect (Frame.Location,
  247. new Size (
  248. value.Size.Width + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal,
  249. value.Size.Height + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical
  250. )
  251. );
  252. }
  253. }
  254. /// <summary>
  255. /// Gets or sets the X position for the view (the column).
  256. /// </summary>
  257. /// <value>The <see cref="Pos"/> object representing the X position.</value>
  258. /// <remarks>
  259. /// <para>
  260. /// If set to a relative value (e.g. <see cref="Pos.Center"/>) the value is indeterminate until the
  261. /// view has been initialized (<see creft="IsInitialized"/> is true) and <see cref="SetRelativeLayout(Rect)"/> has been
  262. /// called.
  263. /// </para>
  264. /// <para>
  265. /// Changing this property will eventually (when the view is next drawn) cause the
  266. /// <see cref="LayoutSubview(View, Rect)"/> and
  267. /// <see cref="OnDrawContent(Rect)"/> methods to be called.
  268. /// </para>
  269. /// <para>
  270. /// Changing this property will cause <see cref="Frame"/> to be updated. If
  271. /// the new value is not of type <see cref="Pos.PosAbsolute"/> the <see cref="LayoutStyle"/> will change to
  272. /// <see cref="LayoutStyle.Computed"/>.
  273. /// </para>
  274. /// <para>
  275. /// The default value is <c>Pos.At (0)</c>.
  276. /// </para>
  277. /// </remarks>
  278. public Pos X {
  279. get => VerifyIsInitialized (_x, nameof (X));
  280. set {
  281. _x = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (X)} cannot be null");
  282. OnResizeNeeded ();
  283. }
  284. }
  285. /// <summary>
  286. /// Gets or sets the Y position for the view (the row).
  287. /// </summary>
  288. /// <value>The <see cref="Pos"/> object representing the Y position.</value>
  289. /// <remarks>
  290. /// <para>
  291. /// If set to a relative value (e.g. <see cref="Pos.Center"/>) the value is indeterminate until the
  292. /// view has been initialized (<see creft="IsInitialized"/> is true) and <see cref="SetRelativeLayout(Rect)"/> has been
  293. /// called.
  294. /// </para>
  295. /// <para>
  296. /// Changing this property will eventually (when the view is next drawn) cause the
  297. /// <see cref="LayoutSubview(View, Rect)"/> and
  298. /// <see cref="OnDrawContent(Rect)"/> methods to be called.
  299. /// </para>
  300. /// <para>
  301. /// Changing this property will cause <see cref="Frame"/> to be updated. If
  302. /// the new value is not of type <see cref="Pos.PosAbsolute"/> the <see cref="LayoutStyle"/> will change to
  303. /// <see cref="LayoutStyle.Computed"/>.
  304. /// </para>
  305. /// <para>
  306. /// The default value is <c>Pos.At (0)</c>.
  307. /// </para>
  308. /// </remarks>
  309. public Pos Y {
  310. get => VerifyIsInitialized (_y, nameof (Y));
  311. set {
  312. _y = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Y)} cannot be null");
  313. OnResizeNeeded ();
  314. }
  315. }
  316. /// <summary>
  317. /// Gets or sets the width of the view.
  318. /// </summary>
  319. /// <value>The <see cref="Dim"/> object representing the width of the view (the number of columns).</value>
  320. /// <remarks>
  321. /// <para>
  322. /// If set to a relative value (e.g. <see cref="Dim.Fill(int)"/>) the value is indeterminate until the
  323. /// view has been initialized (<see creft="IsInitialized"/> is true) and <see cref="SetRelativeLayout(Rect)"/> has been
  324. /// called.
  325. /// </para>
  326. /// <para>
  327. /// Changing this property will eventually (when the view is next drawn) cause the
  328. /// <see cref="LayoutSubview(View, Rect)"/>
  329. /// and <see cref="OnDrawContent(Rect)"/> methods to be called.
  330. /// </para>
  331. /// <para>
  332. /// Changing this property will cause <see cref="Frame"/> to be updated. If
  333. /// the new value is not of type <see cref="Dim.DimAbsolute"/> the <see cref="LayoutStyle"/> will change to
  334. /// <see cref="LayoutStyle.Computed"/>.
  335. /// </para>
  336. /// <para>
  337. /// The default value is <c>Dim.Sized (0)</c>.
  338. /// </para>
  339. /// </remarks>
  340. public Dim Width {
  341. get => VerifyIsInitialized (_width, nameof (Width));
  342. set {
  343. _width = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Width)} cannot be null");
  344. if (ValidatePosDim) {
  345. var isValidNewAutSize = AutoSize && IsValidAutoSizeWidth (_width);
  346. if (IsAdded && AutoSize && !isValidNewAutSize) {
  347. throw new InvalidOperationException ("Must set AutoSize to false before set the Width.");
  348. }
  349. }
  350. OnResizeNeeded ();
  351. }
  352. }
  353. /// <summary>
  354. /// Gets or sets the height of the view.
  355. /// </summary>
  356. /// <value>The <see cref="Dim"/> object representing the height of the view (the number of rows).</value>
  357. /// <remarks>
  358. /// <para>
  359. /// If set to a relative value (e.g. <see cref="Dim.Fill(int)"/>) the value is indeterminate until the
  360. /// view has been initialized (<see creft="IsInitialized"/> is true) and <see cref="SetRelativeLayout(Rect)"/> has been
  361. /// called.
  362. /// </para>
  363. /// <para>
  364. /// Changing this property will eventually (when the view is next drawn) cause the
  365. /// <see cref="LayoutSubview(View, Rect)"/>
  366. /// and <see cref="OnDrawContent(Rect)"/> methods to be called.
  367. /// </para>
  368. /// <para>
  369. /// Changing this property will cause <see cref="Frame"/> to be updated. If
  370. /// the new value is not of type <see cref="Dim.DimAbsolute"/> the <see cref="LayoutStyle"/> will change to
  371. /// <see cref="LayoutStyle.Computed"/>.
  372. /// </para>
  373. /// <para>
  374. /// The default value is <c>Dim.Sized (0)</c>.
  375. /// </para>
  376. /// </remarks>
  377. public Dim Height {
  378. get => VerifyIsInitialized (_height, nameof (Height));
  379. set {
  380. _height = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Height)} cannot be null");
  381. if (ValidatePosDim) {
  382. var isValidNewAutSize = AutoSize && IsValidAutoSizeHeight (_height);
  383. if (IsAdded && AutoSize && !isValidNewAutSize) {
  384. throw new InvalidOperationException ("Must set AutoSize to false before setting the Height.");
  385. }
  386. }
  387. OnResizeNeeded ();
  388. }
  389. }
  390. /// <summary>
  391. /// Gets or sets whether validation of <see cref="Pos"/> and <see cref="Dim"/> occurs.
  392. /// </summary>
  393. /// <remarks>
  394. /// Setting this to <see langword="true"/> will enable validation of <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>,
  395. /// and <see cref="Height"/>
  396. /// during set operations and in <see cref="LayoutSubviews"/>.If invalid settings are discovered exceptions will be thrown
  397. /// indicating the error.
  398. /// This will impose a performance penalty and thus should only be used for debugging.
  399. /// </remarks>
  400. public bool ValidatePosDim { get; set; }
  401. internal bool LayoutNeeded { get; private set; } = true;
  402. /// <summary>
  403. /// Gets or sets a flag that determines whether the View will be automatically resized to fit the <see cref="Text"/>
  404. /// within <see cref="Bounds"/>.
  405. /// <para>
  406. /// The default is <see langword="false"/>. Set to <see langword="true"/> to turn on AutoSize. If <see langword="true"/>
  407. /// then
  408. /// <see cref="Width"/> and <see cref="Height"/> will be used if <see cref="Text"/> can fit;
  409. /// if <see cref="Text"/> won't fit the view will be resized as needed.
  410. /// </para>
  411. /// <para>
  412. /// In addition, if <see cref="ValidatePosDim"/> is <see langword="true"/> the new values of <see cref="Width"/> and
  413. /// <see cref="Height"/> must be of the same types of the existing one to avoid breaking the <see cref="Dim"/> settings.
  414. /// </para>
  415. /// </summary>
  416. public virtual bool AutoSize {
  417. get => _autoSize;
  418. set {
  419. var v = ResizeView (value);
  420. TextFormatter.AutoSize = v;
  421. if (_autoSize != v) {
  422. _autoSize = v;
  423. TextFormatter.NeedsFormat = true;
  424. UpdateTextFormatterText ();
  425. OnResizeNeeded ();
  426. }
  427. }
  428. }
  429. /// <summary>
  430. /// Event called only once when the <see cref="View"/> is being initialized for the first time.
  431. /// Allows configurations and assignments to be performed before the <see cref="View"/> being shown.
  432. /// This derived from <see cref="ISupportInitializeNotification"/> to allow notify all the views that are being
  433. /// initialized.
  434. /// </summary>
  435. public event EventHandler Initialized;
  436. /// <summary>
  437. /// Helper to get the total thickness of the <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/>.
  438. /// </summary>
  439. /// <returns>A thickness that describes the sum of the Frames' thicknesses.</returns>
  440. public Thickness GetFramesThickness ()
  441. {
  442. var left = Margin.Thickness.Left + Border.Thickness.Left + Padding.Thickness.Left;
  443. var top = Margin.Thickness.Top + Border.Thickness.Top + Padding.Thickness.Top;
  444. var right = Margin.Thickness.Right + Border.Thickness.Right + Padding.Thickness.Right;
  445. var bottom = Margin.Thickness.Bottom + Border.Thickness.Bottom + Padding.Thickness.Bottom;
  446. return new Thickness (left, top, right, bottom);
  447. }
  448. /// <summary>
  449. /// Helper to get the X and Y offset of the Bounds from the Frame. This is the sum of the Left and Top properties of
  450. /// <see cref="Margin"/>, <see cref="Border"/> and <see cref="Padding"/>.
  451. /// </summary>
  452. public Point GetBoundsOffset () => new (Padding?.Thickness.GetInside (Padding.Frame).X ?? 0, Padding?.Thickness.GetInside (Padding.Frame).Y ?? 0);
  453. /// <summary>
  454. /// Creates the view's <see cref="Frame"/> objects. This internal method is overridden by Frame to do nothing
  455. /// to prevent recursion during View construction.
  456. /// </summary>
  457. internal virtual void CreateFrames ()
  458. {
  459. void ThicknessChangedHandler (object sender, EventArgs e)
  460. {
  461. if (IsInitialized) {
  462. LayoutFrames ();
  463. }
  464. SetNeedsLayout ();
  465. SetNeedsDisplay ();
  466. }
  467. if (Margin != null) {
  468. Margin.ThicknessChanged -= ThicknessChangedHandler;
  469. Margin.Dispose ();
  470. }
  471. Margin = new Frame { Id = "Margin", Thickness = new Thickness (0) };
  472. Margin.ThicknessChanged += ThicknessChangedHandler;
  473. Margin.Parent = this;
  474. if (Border != null) {
  475. Border.ThicknessChanged -= ThicknessChangedHandler;
  476. Border.Dispose ();
  477. }
  478. Border = new Frame { Id = "Border", Thickness = new Thickness (0) };
  479. Border.ThicknessChanged += ThicknessChangedHandler;
  480. Border.Parent = this;
  481. // TODO: Create View.AddAdornment
  482. if (Padding != null) {
  483. Padding.ThicknessChanged -= ThicknessChangedHandler;
  484. Padding.Dispose ();
  485. }
  486. Padding = new Frame { Id = "Padding", Thickness = new Thickness (0) };
  487. Padding.ThicknessChanged += ThicknessChangedHandler;
  488. Padding.Parent = this;
  489. }
  490. Rect FrameGetInsideBounds ()
  491. {
  492. if (Margin == null || Border == null || Padding == null) {
  493. return new Rect (default, Frame.Size);
  494. }
  495. var width = Math.Max (0, Frame.Size.Width - Margin.Thickness.Horizontal - Border.Thickness.Horizontal - Padding.Thickness.Horizontal);
  496. var height = Math.Max (0, Frame.Size.Height - Margin.Thickness.Vertical - Border.Thickness.Vertical - Padding.Thickness.Vertical);
  497. return new Rect (Point.Empty, new Size (width, height));
  498. }
  499. // Diagnostics to highlight when X or Y is read before the view has been initialized
  500. Pos VerifyIsInitialized (Pos pos, string member)
  501. {
  502. #if DEBUG
  503. if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) {
  504. Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; {member} is indeterminate {pos}. This is potentially a bug.");
  505. }
  506. #endif // DEBUG
  507. return pos;
  508. }
  509. // Diagnostics to highlight when Width or Height is read before the view has been initialized
  510. Dim VerifyIsInitialized (Dim dim, string member)
  511. {
  512. #if DEBUG
  513. if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) {
  514. Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; {member} is indeterminate: {dim}. This is potentially a bug.");
  515. }
  516. #endif // DEBUG
  517. return dim;
  518. }
  519. /// <summary>
  520. /// Called whenever the view needs to be resized. Sets <see cref="Frame"/> and triggers a <see cref="LayoutSubviews()"/>
  521. /// call.
  522. /// </summary>
  523. /// <remarks>
  524. /// <para>
  525. /// Sets the <see cref="Frame"/>.
  526. /// </para>
  527. /// <para>
  528. /// Can be overridden if the view resize behavior is different than the default.
  529. /// </para>
  530. /// </remarks>
  531. protected virtual void OnResizeNeeded ()
  532. {
  533. // First try SuperView.Bounds, then Application.Top, then Driver.Bounds.
  534. // Finally, if none of those are valid, use int.MaxValue (for Unit tests).
  535. var relativeBounds = SuperView is { IsInitialized: true } ? SuperView.Bounds :
  536. Application.Top != null && Application.Top.IsInitialized ? Application.Top.Bounds :
  537. Application.Driver?.Bounds ??
  538. new Rect (0, 0, int.MaxValue, int.MaxValue);
  539. SetRelativeLayout (relativeBounds);
  540. // TODO: Determine what, if any of the below is actually needed here.
  541. if (IsInitialized) {
  542. SetFrameToFitText ();
  543. LayoutFrames ();
  544. TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
  545. SetNeedsLayout ();
  546. SetNeedsDisplay ();
  547. }
  548. }
  549. /// <summary>
  550. /// Sets the internal <see cref="LayoutNeeded"/> flag for this View and all of it's
  551. /// subviews and it's SuperView. The main loop will call SetRelativeLayout and LayoutSubviews
  552. /// for any view with <see cref="LayoutNeeded"/> set.
  553. /// </summary>
  554. internal void SetNeedsLayout ()
  555. {
  556. if (LayoutNeeded) {
  557. return;
  558. }
  559. LayoutNeeded = true;
  560. foreach (var view in Subviews) {
  561. view.SetNeedsLayout ();
  562. }
  563. TextFormatter.NeedsFormat = true;
  564. SuperView?.SetNeedsLayout ();
  565. }
  566. /// <summary>
  567. /// Indicates that the view does not need to be laid out.
  568. /// </summary>
  569. protected void ClearLayoutNeeded () => LayoutNeeded = false;
  570. /// <summary>
  571. /// Converts a screen-relative coordinate to a Frame-relative coordinate. Frame-relative means
  572. /// relative to the View's <see cref="SuperView"/>'s <see cref="Bounds"/>.
  573. /// </summary>
  574. /// <returns>The coordinate relative to the <see cref="SuperView"/>'s <see cref="Bounds"/>.</returns>
  575. /// <param name="x">Screen-relative column.</param>
  576. /// <param name="y">Screen-relative row.</param>
  577. public Point ScreenToFrame (int x, int y)
  578. {
  579. var superViewBoundsOffset = SuperView?.GetBoundsOffset () ?? Point.Empty;
  580. var ret = new Point (x - Frame.X - superViewBoundsOffset.X, y - Frame.Y - superViewBoundsOffset.Y);
  581. if (SuperView != null) {
  582. var superFrame = SuperView.ScreenToFrame (x - superViewBoundsOffset.X, y - superViewBoundsOffset.Y);
  583. ret = new Point (superFrame.X - Frame.X, superFrame.Y - Frame.Y);
  584. }
  585. return ret;
  586. }
  587. /// <summary>
  588. /// Converts a screen-relative coordinate to a bounds-relative coordinate.
  589. /// </summary>
  590. /// <returns>The coordinate relative to this view's <see cref="Bounds"/>.</returns>
  591. /// <param name="x">Screen-relative column.</param>
  592. /// <param name="y">Screen-relative row.</param>
  593. public Point ScreenToBounds (int x, int y)
  594. {
  595. var screen = ScreenToFrame (x, y);
  596. var boundsOffset = GetBoundsOffset ();
  597. return new Point (screen.X - boundsOffset.X, screen.Y - boundsOffset.Y);
  598. }
  599. /// <summary>
  600. /// Converts a <see cref="Bounds"/>-relative coordinate to a screen-relative coordinate. The output is optionally clamped
  601. /// to the screen dimensions.
  602. /// </summary>
  603. /// <param name="x"><see cref="Bounds"/>-relative column.</param>
  604. /// <param name="y"><see cref="Bounds"/>-relative row.</param>
  605. /// <param name="rx">Absolute column; screen-relative.</param>
  606. /// <param name="ry">Absolute row; screen-relative.</param>
  607. /// <param name="clamped">
  608. /// If <see langword="true"/>, <paramref name="rx"/> and <paramref name="ry"/> will be clamped to the
  609. /// screen dimensions (will never be negative and will always be less than <see cref="ConsoleDriver.Cols"/> and
  610. /// <see cref="ConsoleDriver.Rows"/>, respectively.
  611. /// </param>
  612. public virtual void BoundsToScreen (int x, int y, out int rx, out int ry, bool clamped = true)
  613. {
  614. var boundsOffset = GetBoundsOffset ();
  615. rx = x + Frame.X + boundsOffset.X;
  616. ry = y + Frame.Y + boundsOffset.Y;
  617. var super = SuperView;
  618. while (super != null) {
  619. boundsOffset = super.GetBoundsOffset ();
  620. rx += super.Frame.X + boundsOffset.X;
  621. ry += super.Frame.Y + boundsOffset.Y;
  622. super = super.SuperView;
  623. }
  624. // The following ensures that the cursor is always in the screen boundaries.
  625. if (clamped) {
  626. ry = Math.Min (ry, Driver.Rows - 1);
  627. rx = Math.Min (rx, Driver.Cols - 1);
  628. }
  629. }
  630. /// <summary>
  631. /// Converts a <see cref="Bounds"/>-relative region to a screen-relative region.
  632. /// </summary>
  633. public Rect BoundsToScreen (Rect region)
  634. {
  635. BoundsToScreen (region.X, region.Y, out var x, out var y, false);
  636. return new Rect (x, y, region.Width, region.Height);
  637. }
  638. /// <summary>
  639. /// Gets the <see cref="Frame"/> with a screen-relative location.
  640. /// </summary>
  641. /// <returns>The location and size of the view in screen-relative coordinates.</returns>
  642. public virtual Rect FrameToScreen ()
  643. {
  644. var ret = Frame;
  645. var super = SuperView;
  646. while (super != null) {
  647. var boundsOffset = super.GetBoundsOffset ();
  648. ret.X += super.Frame.X + boundsOffset.X;
  649. ret.Y += super.Frame.Y + boundsOffset.Y;
  650. super = super.SuperView;
  651. }
  652. return ret;
  653. }
  654. /// <summary>
  655. /// Applies the view's position (<see cref="X"/>, <see cref="Y"/>) and dimension (<see cref="Width"/>, and
  656. /// <see cref="Height"/>) to
  657. /// <see cref="Frame"/>, given a rectangle describing the SuperView's Bounds (nominally the same as
  658. /// <c>this.SuperView.Bounds</c>).
  659. /// </summary>
  660. /// <param name="superviewBounds">
  661. /// The rectangle describing the SuperView's Bounds (nominally the same as
  662. /// <c>this.SuperView.Bounds</c>).
  663. /// </param>
  664. internal void SetRelativeLayout (Rect superviewBounds)
  665. {
  666. Debug.Assert (_x != null);
  667. Debug.Assert (_y != null);
  668. Debug.Assert (_width != null);
  669. Debug.Assert (_height != null);
  670. int newX, newW, newY, newH;
  671. var autosize = Size.Empty;
  672. if (AutoSize) {
  673. // Note this is global to this function and used as such within the local functions defined
  674. // below. In v2 AutoSize will be re-factored to not need to be dealt with in this function.
  675. autosize = GetAutoSize ();
  676. }
  677. // TODO: Since GetNewLocationAndDimension does not depend on View, it can be moved into PosDim.cs
  678. // TODO: to make architecture more clean. Do this after DimAuto is implemented and the
  679. // TODO: View.AutoSize stuff is removed.
  680. // Returns the new dimension (width or height) and location (x or y) for the View given
  681. // the superview's Bounds
  682. // the current Pos (View.X or View.Y)
  683. // the current Dim (View.Width or View.Height)
  684. // This method is called recursively if pos is Pos.PosCombine
  685. (int newLocation, int newDimension) GetNewLocationAndDimension (bool width, Rect superviewBounds, Pos pos, Dim dim, int autosizeDimension)
  686. {
  687. // Gets the new dimension (width or height, dependent on `width`) of the given Dim given:
  688. // location: the current location (x or y)
  689. // dimension: the new dimension (width or height) (if relevant for Dim type)
  690. // autosize: the size to use if autosize = true
  691. // This method is recursive if d is Dim.DimCombine
  692. int GetNewDimension (Dim d, int location, int dimension, int autosize)
  693. {
  694. int newDimension;
  695. switch (d) {
  696. case Dim.DimCombine combine:
  697. // TODO: Move combine logic into DimCombine?
  698. var leftNewDim = GetNewDimension (combine._left, location, dimension, autosize);
  699. var rightNewDim = GetNewDimension (combine._right, location, dimension, autosize);
  700. if (combine._add) {
  701. newDimension = leftNewDim + rightNewDim;
  702. } else {
  703. newDimension = leftNewDim - rightNewDim;
  704. }
  705. newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
  706. break;
  707. case Dim.DimFactor factor when !factor.IsFromRemaining ():
  708. newDimension = d.Anchor (dimension);
  709. newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
  710. break;
  711. case Dim.DimAbsolute:
  712. // DimAbsoulte.Anchor (int width) ignores width and returns n
  713. newDimension = Math.Max (d.Anchor (0), 0);
  714. newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
  715. break;
  716. case Dim.DimFill:
  717. default:
  718. newDimension = Math.Max (d.Anchor (dimension - location), 0);
  719. newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
  720. break;
  721. }
  722. return newDimension;
  723. }
  724. int newDimension, newLocation;
  725. var superviewDimension = width ? superviewBounds.Width : superviewBounds.Height;
  726. // Determine new location
  727. switch (pos) {
  728. case Pos.PosCenter posCenter:
  729. // For Center, the dimension is dependent on location, but we need to force getting the dimension first
  730. // using a location of 0
  731. newDimension = Math.Max (GetNewDimension (dim, 0, superviewDimension, autosizeDimension), 0);
  732. newLocation = posCenter.Anchor (superviewDimension - newDimension);
  733. newDimension = Math.Max (GetNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0);
  734. break;
  735. case Pos.PosCombine combine:
  736. // TODO: Move combine logic into PosCombine?
  737. int left, right;
  738. (left, newDimension) = GetNewLocationAndDimension (width, superviewBounds, combine._left, dim, autosizeDimension);
  739. (right, newDimension) = GetNewLocationAndDimension (width, superviewBounds, combine._right, dim, autosizeDimension);
  740. if (combine._add) {
  741. newLocation = left + right;
  742. } else {
  743. newLocation = left - right;
  744. }
  745. newDimension = Math.Max (GetNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0);
  746. break;
  747. case Pos.PosAnchorEnd:
  748. case Pos.PosAbsolute:
  749. case Pos.PosFactor:
  750. case Pos.PosFunc:
  751. case Pos.PosView:
  752. default:
  753. newLocation = pos?.Anchor (superviewDimension) ?? 0;
  754. newDimension = Math.Max (GetNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0);
  755. break;
  756. }
  757. return (newLocation, newDimension);
  758. }
  759. // horizontal/width
  760. (newX, newW) = GetNewLocationAndDimension (true, superviewBounds, _x, _width, autosize.Width);
  761. // vertical/height
  762. (newY, newH) = GetNewLocationAndDimension (false, superviewBounds, _y, _height, autosize.Height);
  763. var r = new Rect (newX, newY, newW, newH);
  764. if (Frame != r) {
  765. // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height, making
  766. // the view LayoutStyle.Absolute.
  767. _frame = r;
  768. if (_x is Pos.PosAbsolute) {
  769. _x = Frame.X;
  770. }
  771. if (_y is Pos.PosAbsolute) {
  772. _y = Frame.Y;
  773. }
  774. if (_width is Dim.DimAbsolute) {
  775. _width = Frame.Width;
  776. }
  777. if (_height is Dim.DimAbsolute) {
  778. _height = Frame.Height;
  779. }
  780. if (IsInitialized) {
  781. // TODO: Figure out what really is needed here. All unit tests (except AutoSize) pass as-is
  782. //LayoutFrames ();
  783. //TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
  784. SetNeedsLayout ();
  785. //SetNeedsDisplay ();
  786. }
  787. // BUGBUG: Why is this AFTER setting Frame? Seems duplicative.
  788. if (!SetFrameToFitText ()) {
  789. TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
  790. }
  791. }
  792. }
  793. /// <summary>
  794. /// Fired after the View's <see cref="LayoutSubviews"/> method has completed.
  795. /// </summary>
  796. /// <remarks>
  797. /// Subscribe to this event to perform tasks when the <see cref="View"/> has been resized or the layout has otherwise
  798. /// changed.
  799. /// </remarks>
  800. public event EventHandler<LayoutEventArgs> LayoutStarted;
  801. /// <summary>
  802. /// Raises the <see cref="LayoutStarted"/> event. Called from <see cref="LayoutSubviews"/> before any subviews have been
  803. /// laid out.
  804. /// </summary>
  805. internal virtual void OnLayoutStarted (LayoutEventArgs args) => LayoutStarted?.Invoke (this, args);
  806. /// <summary>
  807. /// Fired after the View's <see cref="LayoutSubviews"/> method has completed.
  808. /// </summary>
  809. /// <remarks>
  810. /// Subscribe to this event to perform tasks when the <see cref="View"/> has been resized or the layout has otherwise
  811. /// changed.
  812. /// </remarks>
  813. public event EventHandler<LayoutEventArgs> LayoutComplete;
  814. /// <summary>
  815. /// Raises the <see cref="LayoutComplete"/> event. Called from <see cref="LayoutSubviews"/> before all sub-views have been
  816. /// laid out.
  817. /// </summary>
  818. internal virtual void OnLayoutComplete (LayoutEventArgs args) => LayoutComplete?.Invoke (this, args);
  819. internal void CollectPos (Pos pos, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
  820. {
  821. switch (pos) {
  822. case Pos.PosView pv:
  823. // See #2461
  824. //if (!from.InternalSubviews.Contains (pv.Target)) {
  825. // throw new InvalidOperationException ($"View {pv.Target} is not a subview of {from}");
  826. //}
  827. if (pv.Target != this) {
  828. nEdges.Add ((pv.Target, from));
  829. }
  830. return;
  831. case Pos.PosCombine pc:
  832. CollectPos (pc._left, from, ref nNodes, ref nEdges);
  833. CollectPos (pc._right, from, ref nNodes, ref nEdges);
  834. break;
  835. }
  836. }
  837. internal void CollectDim (Dim dim, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
  838. {
  839. switch (dim) {
  840. case Dim.DimView dv:
  841. // See #2461
  842. //if (!from.InternalSubviews.Contains (dv.Target)) {
  843. // throw new InvalidOperationException ($"View {dv.Target} is not a subview of {from}");
  844. //}
  845. if (dv.Target != this) {
  846. nEdges.Add ((dv.Target, from));
  847. }
  848. return;
  849. case Dim.DimCombine dc:
  850. CollectDim (dc._left, from, ref nNodes, ref nEdges);
  851. CollectDim (dc._right, from, ref nNodes, ref nEdges);
  852. break;
  853. }
  854. }
  855. internal void CollectAll (View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
  856. {
  857. // BUGBUG: This should really only work on initialized subviews
  858. foreach (var v in from.InternalSubviews /*.Where(v => v.IsInitialized)*/) {
  859. nNodes.Add (v);
  860. if (v.LayoutStyle != LayoutStyle.Computed) {
  861. continue;
  862. }
  863. CollectPos (v.X, v, ref nNodes, ref nEdges);
  864. CollectPos (v.Y, v, ref nNodes, ref nEdges);
  865. CollectDim (v.Width, v, ref nNodes, ref nEdges);
  866. CollectDim (v.Height, v, ref nNodes, ref nEdges);
  867. }
  868. }
  869. // https://en.wikipedia.org/wiki/Topological_sorting
  870. internal static List<View> TopologicalSort (View superView, IEnumerable<View> nodes, ICollection<(View From, View To)> edges)
  871. {
  872. var result = new List<View> ();
  873. // Set of all nodes with no incoming edges
  874. var noEdgeNodes = new HashSet<View> (nodes.Where (n => edges.All (e => !e.To.Equals (n))));
  875. while (noEdgeNodes.Any ()) {
  876. // remove a node n from S
  877. var n = noEdgeNodes.First ();
  878. noEdgeNodes.Remove (n);
  879. // add n to tail of L
  880. if (n != superView) {
  881. result.Add (n);
  882. }
  883. // for each node m with an edge e from n to m do
  884. foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) {
  885. var m = e.To;
  886. // remove edge e from the graph
  887. edges.Remove (e);
  888. // if m has no other incoming edges then
  889. if (edges.All (me => !me.To.Equals (m)) && m != superView) {
  890. // insert m into S
  891. noEdgeNodes.Add (m);
  892. }
  893. }
  894. }
  895. if (!edges.Any ()) {
  896. return result;
  897. }
  898. foreach ((var from, var to) in edges) {
  899. if (from == to) {
  900. // if not yet added to the result, add it and remove from edge
  901. if (result.Find (v => v == from) == null) {
  902. result.Add (from);
  903. }
  904. edges.Remove ((from, to));
  905. } else if (from.SuperView == to.SuperView) {
  906. // if 'from' is not yet added to the result, add it
  907. if (result.Find (v => v == from) == null) {
  908. result.Add (from);
  909. }
  910. // if 'to' is not yet added to the result, add it
  911. if (result.Find (v => v == to) == null) {
  912. result.Add (to);
  913. }
  914. // remove from edge
  915. edges.Remove ((from, to));
  916. } else if (from != superView?.GetTopSuperView (to, from) && !ReferenceEquals (from, to)) {
  917. if (ReferenceEquals (from.SuperView, to)) {
  918. throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{to}\" references a SubView (\"{from}\").");
  919. }
  920. throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{from}\" linked with \"{to}\" was not found. Did you forget to add it to {superView}?");
  921. }
  922. }
  923. // return L (a topologically sorted order)
  924. return result;
  925. } // TopologicalSort
  926. /// <summary>
  927. /// Overriden by <see cref="Frame"/> to do nothing, as the <see cref="Frame"/> does not have frames.
  928. /// </summary>
  929. internal virtual void LayoutFrames ()
  930. {
  931. if (Margin == null) {
  932. return; // CreateFrames() has not been called yet
  933. }
  934. if (Margin.Frame.Size != Frame.Size) {
  935. Margin._frame = new Rect (Point.Empty, Frame.Size);
  936. Margin.X = 0;
  937. Margin.Y = 0;
  938. Margin.Width = Frame.Size.Width;
  939. Margin.Height = Frame.Size.Height;
  940. Margin.SetNeedsLayout ();
  941. Margin.SetNeedsDisplay ();
  942. }
  943. var border = Margin.Thickness.GetInside (Margin.Frame);
  944. if (border != Border.Frame) {
  945. Border._frame = new Rect (new Point (border.Location.X, border.Location.Y), border.Size);
  946. Border.X = border.Location.X;
  947. Border.Y = border.Location.Y;
  948. Border.Width = border.Size.Width;
  949. Border.Height = border.Size.Height;
  950. Border.SetNeedsLayout ();
  951. Border.SetNeedsDisplay ();
  952. }
  953. var padding = Border.Thickness.GetInside (Border.Frame);
  954. if (padding != Padding.Frame) {
  955. Padding._frame = new Rect (new Point (padding.Location.X, padding.Location.Y), padding.Size);
  956. Padding.X = padding.Location.X;
  957. Padding.Y = padding.Location.Y;
  958. Padding.Width = padding.Size.Width;
  959. Padding.Height = padding.Size.Height;
  960. Padding.SetNeedsLayout ();
  961. Padding.SetNeedsDisplay ();
  962. }
  963. }
  964. /// <summary>
  965. /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in
  966. /// response to the container view or terminal resizing.
  967. /// </summary>
  968. /// <remarks>
  969. /// <para>
  970. /// The position and dimensions of the view are indeterminate until the view has been initialized. Therefore,
  971. /// the behavior of this method is indeterminate if <see cref="IsInitialized"/> is <see langword="false"/>.
  972. /// </para>
  973. /// <para>
  974. /// Raises the <see cref="LayoutComplete"/> event) before it returns.
  975. /// </para>
  976. /// </remarks>
  977. public virtual void LayoutSubviews ()
  978. {
  979. if (!IsInitialized) {
  980. Debug.WriteLine ($"WARNING: LayoutSubviews called before view has been initialized. This is likely a bug in {this}");
  981. }
  982. if (!LayoutNeeded) {
  983. return;
  984. }
  985. LayoutFrames ();
  986. var oldBounds = Bounds;
  987. OnLayoutStarted (new LayoutEventArgs { OldBounds = oldBounds });
  988. TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
  989. // Sort out the dependencies of the X, Y, Width, Height properties
  990. var nodes = new HashSet<View> ();
  991. var edges = new HashSet<(View, View)> ();
  992. CollectAll (this, ref nodes, ref edges);
  993. var ordered = TopologicalSort (SuperView, nodes, edges);
  994. foreach (var v in ordered) {
  995. LayoutSubview (v, new Rect (GetBoundsOffset (), Bounds.Size));
  996. }
  997. // If the 'to' is rooted to 'from' and the layoutstyle is Computed it's a special-case.
  998. // Use LayoutSubview with the Frame of the 'from'
  999. if (SuperView != null && GetTopSuperView () != null && LayoutNeeded && edges.Count > 0) {
  1000. foreach ((var from, var to) in edges) {
  1001. LayoutSubview (to, from.Frame);
  1002. }
  1003. }
  1004. LayoutNeeded = false;
  1005. OnLayoutComplete (new LayoutEventArgs { OldBounds = oldBounds });
  1006. }
  1007. void LayoutSubview (View v, Rect contentArea)
  1008. {
  1009. //if (v.LayoutStyle == LayoutStyle.Computed) {
  1010. v.SetRelativeLayout (contentArea);
  1011. //}
  1012. v.LayoutSubviews ();
  1013. v.LayoutNeeded = false;
  1014. }
  1015. bool ResizeView (bool autoSize)
  1016. {
  1017. if (!autoSize) {
  1018. return false;
  1019. }
  1020. var boundsChanged = true;
  1021. var newFrameSize = GetAutoSize ();
  1022. if (IsInitialized && newFrameSize != Frame.Size) {
  1023. if (ValidatePosDim) {
  1024. // BUGBUG: This ain't right, obviously. We need to figure out how to handle this.
  1025. boundsChanged = ResizeBoundsToFit (newFrameSize);
  1026. } else {
  1027. Height = newFrameSize.Height;
  1028. Width = newFrameSize.Width;
  1029. }
  1030. }
  1031. return boundsChanged;
  1032. }
  1033. /// <summary>
  1034. /// Resizes the View to fit the specified size. Factors in the HotKey.
  1035. /// </summary>
  1036. /// <param name="size"></param>
  1037. /// <returns>whether the Bounds was changed or not</returns>
  1038. bool ResizeBoundsToFit (Size size)
  1039. {
  1040. var boundsChanged = false;
  1041. var canSizeW = TrySetWidth (size.Width - GetHotKeySpecifierLength (), out var rW);
  1042. var canSizeH = TrySetHeight (size.Height - GetHotKeySpecifierLength (false), out var rH);
  1043. if (canSizeW) {
  1044. boundsChanged = true;
  1045. _width = rW;
  1046. }
  1047. if (canSizeH) {
  1048. boundsChanged = true;
  1049. _height = rH;
  1050. }
  1051. if (boundsChanged) {
  1052. Bounds = new Rect (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height);
  1053. }
  1054. return boundsChanged;
  1055. }
  1056. /// <summary>
  1057. /// Gets the Frame dimensions required to fit <see cref="Text"/> within <see cref="Bounds"/> using the text
  1058. /// <see cref="Direction"/> specified by the
  1059. /// <see cref="TextFormatter"/> property and accounting for any <see cref="HotKeySpecifier"/> characters.
  1060. /// </summary>
  1061. /// <returns>The <see cref="Size"/> of the view required to fit the text.</returns>
  1062. public Size GetAutoSize ()
  1063. {
  1064. var x = 0;
  1065. var y = 0;
  1066. if (IsInitialized) {
  1067. x = Bounds.X;
  1068. y = Bounds.Y;
  1069. }
  1070. var rect = TextFormatter.CalcRect (x, y, TextFormatter.Text, TextFormatter.Direction);
  1071. var newWidth = rect.Size.Width - GetHotKeySpecifierLength () + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal;
  1072. var newHeight = rect.Size.Height - GetHotKeySpecifierLength (false) + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical;
  1073. return new Size (newWidth, newHeight);
  1074. }
  1075. bool IsValidAutoSize (out Size autoSize)
  1076. {
  1077. var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
  1078. autoSize = new Size (rect.Size.Width - GetHotKeySpecifierLength (),
  1079. rect.Size.Height - GetHotKeySpecifierLength (false));
  1080. return !(ValidatePosDim && (!(Width is Dim.DimAbsolute) || !(Height is Dim.DimAbsolute)) ||
  1081. _frame.Size.Width != rect.Size.Width - GetHotKeySpecifierLength () ||
  1082. _frame.Size.Height != rect.Size.Height - GetHotKeySpecifierLength (false));
  1083. }
  1084. bool IsValidAutoSizeWidth (Dim width)
  1085. {
  1086. var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
  1087. var dimValue = width.Anchor (0);
  1088. return !(ValidatePosDim && !(width is Dim.DimAbsolute) || dimValue != rect.Size.Width - GetHotKeySpecifierLength ());
  1089. }
  1090. bool IsValidAutoSizeHeight (Dim height)
  1091. {
  1092. var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
  1093. var dimValue = height.Anchor (0);
  1094. return !(ValidatePosDim && !(height is Dim.DimAbsolute) || dimValue != rect.Size.Height - GetHotKeySpecifierLength (false));
  1095. }
  1096. /// <summary>
  1097. /// Determines if the View's <see cref="Width"/> can be set to a new value.
  1098. /// </summary>
  1099. /// <param name="desiredWidth"></param>
  1100. /// <param name="resultWidth">
  1101. /// Contains the width that would result if <see cref="Width"/> were set to
  1102. /// <paramref name="desiredWidth"/>"/>
  1103. /// </param>
  1104. /// <returns>
  1105. /// <see langword="true"/> if the View's <see cref="Width"/> can be changed to the specified value. False
  1106. /// otherwise.
  1107. /// </returns>
  1108. internal bool TrySetWidth (int desiredWidth, out int resultWidth)
  1109. {
  1110. var w = desiredWidth;
  1111. bool canSetWidth;
  1112. switch (Width) {
  1113. case Dim.DimCombine _:
  1114. case Dim.DimView _:
  1115. case Dim.DimFill _:
  1116. // It's a Dim.DimCombine and so can't be assigned. Let it have it's Width anchored.
  1117. w = Width.Anchor (w);
  1118. canSetWidth = !ValidatePosDim;
  1119. break;
  1120. case Dim.DimFactor factor:
  1121. // Tries to get the SuperView Width otherwise the view Width.
  1122. var sw = SuperView != null ? SuperView.Frame.Width : w;
  1123. if (factor.IsFromRemaining ()) {
  1124. sw -= Frame.X;
  1125. }
  1126. w = Width.Anchor (sw);
  1127. canSetWidth = !ValidatePosDim;
  1128. break;
  1129. default:
  1130. canSetWidth = true;
  1131. break;
  1132. }
  1133. resultWidth = w;
  1134. return canSetWidth;
  1135. }
  1136. /// <summary>
  1137. /// Determines if the View's <see cref="Height"/> can be set to a new value.
  1138. /// </summary>
  1139. /// <param name="desiredHeight"></param>
  1140. /// <param name="resultHeight">
  1141. /// Contains the width that would result if <see cref="Height"/> were set to
  1142. /// <paramref name="desiredHeight"/>"/>
  1143. /// </param>
  1144. /// <returns>
  1145. /// <see langword="true"/> if the View's <see cref="Height"/> can be changed to the specified value. False
  1146. /// otherwise.
  1147. /// </returns>
  1148. internal bool TrySetHeight (int desiredHeight, out int resultHeight)
  1149. {
  1150. var h = desiredHeight;
  1151. bool canSetHeight;
  1152. switch (Height) {
  1153. case Dim.DimCombine _:
  1154. case Dim.DimView _:
  1155. case Dim.DimFill _:
  1156. // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored.
  1157. h = Height.Anchor (h);
  1158. canSetHeight = !ValidatePosDim;
  1159. break;
  1160. case Dim.DimFactor factor:
  1161. // Tries to get the SuperView height otherwise the view height.
  1162. var sh = SuperView != null ? SuperView.Frame.Height : h;
  1163. if (factor.IsFromRemaining ()) {
  1164. sh -= Frame.Y;
  1165. }
  1166. h = Height.Anchor (sh);
  1167. canSetHeight = !ValidatePosDim;
  1168. break;
  1169. default:
  1170. canSetHeight = true;
  1171. break;
  1172. }
  1173. resultHeight = h;
  1174. return canSetHeight;
  1175. }
  1176. /// <summary>
  1177. /// Finds which view that belong to the <paramref name="start"/> superview at the provided location.
  1178. /// </summary>
  1179. /// <param name="start">The superview where to look for.</param>
  1180. /// <param name="x">The column location in the superview.</param>
  1181. /// <param name="y">The row location in the superview.</param>
  1182. /// <param name="resx">The found view screen relative column location.</param>
  1183. /// <param name="resy">The found view screen relative row location.</param>
  1184. /// <returns>
  1185. /// The view that was found at the <praramref name="x"/> and <praramref name="y"/> coordinates.
  1186. /// <see langword="null"/> if no view was found.
  1187. /// </returns>
  1188. public static View FindDeepestView (View start, int x, int y, out int resx, out int resy)
  1189. {
  1190. resy = resx = 0;
  1191. if (start == null || !start.Frame.Contains (x, y)) {
  1192. return null;
  1193. }
  1194. var startFrame = start.Frame;
  1195. if (start.InternalSubviews != null) {
  1196. var count = start.InternalSubviews.Count;
  1197. if (count > 0) {
  1198. var boundsOffset = start.GetBoundsOffset ();
  1199. var rx = x - (startFrame.X + boundsOffset.X);
  1200. var ry = y - (startFrame.Y + boundsOffset.Y);
  1201. for (var i = count - 1; i >= 0; i--) {
  1202. var v = start.InternalSubviews [i];
  1203. if (v.Visible && v.Frame.Contains (rx, ry)) {
  1204. var deep = FindDeepestView (v, rx, ry, out resx, out resy);
  1205. if (deep == null) {
  1206. return v;
  1207. }
  1208. return deep;
  1209. }
  1210. }
  1211. }
  1212. }
  1213. resx = x - startFrame.X;
  1214. resy = y - startFrame.Y;
  1215. return start;
  1216. }
  1217. }