2
0

ViewLayout.cs 59 KB

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