ViewLayout.cs 59 KB

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