ViewContent.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. using System.Diagnostics;
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Settings for how scrolling the <see cref="View.Viewport"/> on the View's Content Area is handled.
  5. /// </summary>
  6. [Flags]
  7. public enum ScrollSettings
  8. {
  9. /// <summary>
  10. /// No settings.
  11. /// </summary>
  12. None = 0,
  13. /// <summary>
  14. /// If set, <c>Viewport.Location.Y</c> can be negative or greater than to <see cref="View.ContentSize"/>.<c>Height</c>,
  15. /// enabling scrolling beyond the dimensions of the content area vertically.
  16. /// </summary>
  17. AllowViewportYBeyondContent = 1,
  18. /// <summary>
  19. /// If set, <c>Viewport.Location.X</c> can be negative or greater than to <see cref="View.ContentSize"/>.<c>Width</c>,
  20. /// enabling scrolling beyond the dimensions of the content area horizontally.
  21. /// </summary>
  22. AllowViewportXBeyondContent = 2,
  23. /// <summary>
  24. /// If set, <c>Viewport.Location</c> can be negative or greater than to <see cref="View.ContentSize"/>,
  25. /// enabling scrolling beyond the dimensions of the content area either horizontally or vertically.
  26. /// </summary>
  27. AllowViewportLocationBeyondContent = AllowViewportYBeyondContent | AllowViewportXBeyondContent
  28. }
  29. public partial class View
  30. {
  31. #region Content Area
  32. private Size _contentSize;
  33. /// <summary>
  34. /// Gets or sets the size of the View's content. If the value is <c>Size.Empty</c> the size of the content is
  35. /// the same as the size of <see cref="Viewport"/>, and <c>Viewport.Location</c> will always be <c>0, 0</c>.
  36. /// </summary>
  37. /// <remarks>
  38. /// <para>
  39. /// If a positive size is provided, <see cref="Viewport"/> describes the portion of the content currently visible
  40. /// to the view. This enables virtual scrolling.
  41. /// </para>
  42. /// <para>
  43. /// Negative sizes are not supported.
  44. /// </para>
  45. /// </remarks>
  46. public Size ContentSize
  47. {
  48. get => _contentSize == Size.Empty ? Viewport.Size : _contentSize;
  49. set
  50. {
  51. if (value.Width < 0 || value.Height < 0)
  52. {
  53. throw new ArgumentException (@"ContentSize cannot be negative.", nameof (value));
  54. }
  55. if (value == _contentSize)
  56. {
  57. return;
  58. }
  59. _contentSize = value;
  60. OnContentSizeChanged (new (_contentSize));
  61. }
  62. }
  63. /// <summary>
  64. /// Called when <see cref="ContentSize"/> changes. Invokes the <see cref="ContentSizeChanged"/> event.
  65. /// </summary>
  66. /// <param name="e"></param>
  67. /// <returns></returns>
  68. protected bool? OnContentSizeChanged (SizeChangedEventArgs e)
  69. {
  70. ContentSizeChanged?.Invoke (this, e);
  71. if (e.Cancel != true)
  72. {
  73. SetNeedsDisplay ();
  74. }
  75. return e.Cancel;
  76. }
  77. /// <summary>
  78. /// Event that is raised when the <see cref="ContentSize"/> changes.
  79. /// </summary>
  80. public event EventHandler<SizeChangedEventArgs> ContentSizeChanged;
  81. /// <summary>
  82. /// Converts a Content-relative location to a Screen-relative location.
  83. /// </summary>
  84. /// <param name="location">The Content-relative location.</param>
  85. /// <returns>The Screen-relative location.</returns>
  86. public Point ContentToScreen (in Point location)
  87. {
  88. // Subtract the ViewportOffsetFromFrame to get the Viewport-relative location.
  89. Point viewportOffset = GetViewportOffsetFromFrame ();
  90. Point contentRelativeToViewport = location;
  91. contentRelativeToViewport.Offset (-Viewport.X, -Viewport.Y);
  92. // Translate to Screen-Relative (our SuperView's Viewport-relative coordinates)
  93. Rectangle screen = ViewportToScreen (new (contentRelativeToViewport, Size.Empty));
  94. return screen.Location;
  95. }
  96. /// <summary>Converts a Screen-relative coordinate to a Content-relative coordinate.</summary>
  97. /// <remarks>
  98. /// Content-relative means relative to the top-left corner of the view's Content, which is
  99. /// always at <c>0, 0</c>.
  100. /// </remarks>
  101. /// <param name="x">Column relative to the left side of the Content.</param>
  102. /// <param name="y">Row relative to the top of the Content</param>
  103. /// <returns>The coordinate relative to this view's Content.</returns>
  104. public Point ScreenToContent (in Point location)
  105. {
  106. Point viewportOffset = GetViewportOffsetFromFrame ();
  107. Point screen = ScreenToFrame (location.X, location.Y);
  108. screen.Offset (Viewport.X - viewportOffset.X, Viewport.Y - viewportOffset.Y);
  109. return screen;
  110. }
  111. #endregion Content Area
  112. #region Viewport
  113. private ScrollSettings _scrollSettings;
  114. /// <summary>
  115. /// Gets or sets how scrolling the <see cref="View.Viewport"/> on the View's Content Area is handled.
  116. /// </summary>
  117. public ScrollSettings ScrollSettings
  118. {
  119. get => _scrollSettings;
  120. set
  121. {
  122. if (_scrollSettings == value)
  123. {
  124. return;
  125. }
  126. _scrollSettings = value;
  127. // Force set Viewport to cause settings to be applied as needed
  128. SetViewport (Viewport);
  129. }
  130. }
  131. /// <summary>
  132. /// The location of the viewport into the view's content (0,0) is the top-left corner of the content. The Content
  133. /// area's size
  134. /// is <see cref="ContentSize"/>.
  135. /// </summary>
  136. private Point _viewportLocation;
  137. /// <summary>
  138. /// Gets or sets the rectangle describing the portion of the View's content that is visible to the user.
  139. /// The viewport Location is relative to the top-left corner of the inner rectangle of <see cref="Padding"/>.
  140. /// If the viewport Size is the same as <see cref="ContentSize"/> the Location will be <c>0, 0</c>.
  141. /// </summary>
  142. /// <value>
  143. /// The rectangle describing the location and size of the viewport into the View's virtual content, described by
  144. /// <see cref="ContentSize"/>.
  145. /// </value>
  146. /// <remarks>
  147. /// <para>
  148. /// Positive values for the location indicate the visible area is offset into (down-and-right) the View's virtual
  149. /// <see cref="ContentSize"/>. This enables virtual scrolling.
  150. /// </para>
  151. /// <para>
  152. /// Negative values for the location indicate the visible area is offset above (up-and-left) the View's virtual
  153. /// <see cref="ContentSize"/>. This enables virtual zoom.
  154. /// </para>
  155. /// <para>
  156. /// The <see cref="ScrollSettings"/> property controls how scrolling is handled. If <see cref="ScrollSettings"/> is
  157. /// </para>
  158. /// <para>
  159. /// If <see cref="LayoutStyle"/> is <see cref="LayoutStyle.Computed"/> the value of Viewport is indeterminate until
  160. /// the view has been initialized ( <see cref="IsInitialized"/> is true) and <see cref="LayoutSubviews"/> has been
  161. /// called.
  162. /// </para>
  163. /// <para>
  164. /// Updates to the Viewport Size updates <see cref="Frame"/>, and has the same impact as updating the
  165. /// <see cref="Frame"/>.
  166. /// </para>
  167. /// <para>
  168. /// Altering the Viewport Size will eventually (when the view is next laid out) cause the
  169. /// <see cref="LayoutSubview(View, Size)"/> and <see cref="OnDrawContent(Rectangle)"/> methods to be called.
  170. /// </para>
  171. /// </remarks>
  172. public virtual Rectangle Viewport
  173. {
  174. get
  175. {
  176. #if DEBUG
  177. if (LayoutStyle == LayoutStyle.Computed && !IsInitialized)
  178. {
  179. Debug.WriteLine (
  180. $"WARNING: Viewport is being accessed before the View has been initialized. This is likely a bug in {this}"
  181. );
  182. }
  183. #endif // DEBUG
  184. if (Margin is null || Border is null || Padding is null)
  185. {
  186. // CreateAdornments has not been called yet.
  187. return new (_viewportLocation, Frame.Size);
  188. }
  189. Thickness thickness = GetAdornmentsThickness ();
  190. return new (
  191. _viewportLocation,
  192. new (
  193. Math.Max (0, Frame.Size.Width - thickness.Horizontal),
  194. Math.Max (0, Frame.Size.Height - thickness.Vertical)
  195. ));
  196. }
  197. set => SetViewport (value);
  198. }
  199. private void SetViewport (Rectangle viewport)
  200. {
  201. ApplySettings (ref viewport);
  202. Thickness thickness = GetAdornmentsThickness ();
  203. Size newSize = new (
  204. viewport.Size.Width + thickness.Horizontal,
  205. viewport.Size.Height + thickness.Vertical);
  206. if (newSize == Frame.Size)
  207. {
  208. // The change is not changing the Frame, so we don't need to update it.
  209. // Just call SetNeedsLayout to update the layout.
  210. if (_viewportLocation != viewport.Location)
  211. {
  212. _viewportLocation = viewport.Location;
  213. SetNeedsLayout ();
  214. }
  215. return;
  216. }
  217. _viewportLocation = viewport.Location;
  218. // Update the Frame because we made it bigger or smaller which impacts subviews.
  219. Frame = Frame with
  220. {
  221. Size = newSize
  222. };
  223. void ApplySettings (ref Rectangle location)
  224. {
  225. if (!ScrollSettings.HasFlag (ScrollSettings.AllowViewportYBeyondContent))
  226. {
  227. if (location.Y + Viewport.Height > ContentSize.Height)
  228. {
  229. location.Y = ContentSize.Height - Viewport.Height;
  230. }
  231. if (location.Y < 0)
  232. {
  233. location.Y = 0;
  234. }
  235. }
  236. if (!ScrollSettings.HasFlag (ScrollSettings.AllowViewportXBeyondContent))
  237. {
  238. if (location.X + Viewport.Width > ContentSize.Width)
  239. {
  240. location.X = ContentSize.Width - Viewport.Width;
  241. }
  242. if (location.X < 0)
  243. {
  244. location.X = 0;
  245. }
  246. }
  247. }
  248. }
  249. /// <summary>
  250. /// Converts a <see cref="Viewport"/>-relative location to a screen-relative location.
  251. /// </summary>
  252. /// <remarks>
  253. /// Viewport-relative means relative to the top-left corner of the inner rectangle of the <see cref="Padding"/>.
  254. /// </remarks>
  255. public Rectangle ViewportToScreen (in Rectangle location)
  256. {
  257. // Translate bounds to Frame (our SuperView's Viewport-relative coordinates)
  258. Rectangle screen = FrameToScreen ();
  259. Point viewportOffset = GetViewportOffsetFromFrame ();
  260. screen.Offset (viewportOffset.X + location.X, viewportOffset.Y + location.Y);
  261. return new (screen.Location, location.Size);
  262. }
  263. /// <summary>Converts a screen-relative coordinate to a Viewport-relative coordinate.</summary>
  264. /// <returns>The coordinate relative to this view's <see cref="Viewport"/>.</returns>
  265. /// <remarks>
  266. /// Viewport-relative means relative to the top-left corner of the inner rectangle of the <see cref="Padding"/>.
  267. /// </remarks>
  268. /// <param name="x">Column relative to the left side of the Viewport.</param>
  269. /// <param name="y">Row relative to the top of the Viewport</param>
  270. public Point ScreenToViewport (int x, int y)
  271. {
  272. Point viewportOffset = GetViewportOffsetFromFrame ();
  273. Point screen = ScreenToFrame (x, y);
  274. screen.Offset (-viewportOffset.X, -viewportOffset.Y);
  275. return screen;
  276. }
  277. /// <summary>
  278. /// Helper to get the X and Y offset of the Viewport from the Frame. This is the sum of the Left and Top properties
  279. /// of <see cref="Margin"/>, <see cref="Border"/> and <see cref="Padding"/>.
  280. /// </summary>
  281. public Point GetViewportOffsetFromFrame () { return Padding is null ? Point.Empty : Padding.Thickness.GetInside (Padding.Frame).Location; }
  282. /// <summary>
  283. /// Scrolls the view vertically by the specified number of rows.
  284. /// </summary>
  285. /// <remarks>
  286. /// <para>
  287. /// </para>
  288. /// </remarks>
  289. /// <param name="rows"></param>
  290. /// <returns><see langword="true"/> if the <see cref="Viewport"/> was changed.</returns>
  291. public bool? ScrollVertical (int rows)
  292. {
  293. if (ContentSize == Size.Empty || ContentSize == Viewport.Size)
  294. {
  295. return false;
  296. }
  297. Viewport = Viewport with { Y = Viewport.Y + rows };
  298. return true;
  299. }
  300. /// <summary>
  301. /// Scrolls the view horizontally by the specified number of columns.
  302. /// </summary>
  303. /// <remarks>
  304. /// <para>
  305. /// </para>
  306. /// </remarks>
  307. /// <param name="cols"></param>
  308. /// <returns><see langword="true"/> if the <see cref="Viewport"/> was changed.</returns>
  309. public bool? ScrollHorizontal (int cols)
  310. {
  311. if (ContentSize == Size.Empty || ContentSize == Viewport.Size)
  312. {
  313. return false;
  314. }
  315. Viewport = Viewport with { X = Viewport.X + cols };
  316. return true;
  317. }
  318. #endregion Viewport
  319. }