Frame.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. using System.Text;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. using System.Linq;
  6. using System.Xml.Linq;
  7. using static Terminal.Gui.TileView;
  8. namespace Terminal.Gui {
  9. // TODO: v2 - Missing 3D effect - 3D effects will be drawn by a mechanism separate from Frames
  10. // TODO: v2 - If a Frame has focus, navigation keys (e.g Command.NextView) should cycle through SubViews of the Frame
  11. // QUESTION: How does a user navigate out of a Frame to another Frame, or back into the Parent's SubViews?
  12. /// <summary>
  13. /// Frames are a special form of <see cref="View"/> that act as adornments; they appear outside of the <see cref="View.Bounds"/>
  14. /// enabling borders, menus, etc...
  15. /// </summary>
  16. public class Frame : View {
  17. private Thickness _thickness = Thickness.Empty;
  18. internal override void CreateFrames () { /* Do nothing - Frames do not have Frames */ }
  19. internal override void LayoutFrames () { /* Do nothing - Frames do not have Frames */ }
  20. /// <summary>
  21. /// The Parent of this Frame (the View this Frame surrounds).
  22. /// </summary>
  23. public View Parent { get; set; }
  24. /// <summary>
  25. /// Frames cannot be used as sub-views, so this method always throws an <see cref="InvalidOperationException"/>.
  26. /// TODO: Are we sure?
  27. /// </summary>
  28. public override View SuperView {
  29. get {
  30. return null;
  31. }
  32. set {
  33. throw new NotImplementedException ();
  34. }
  35. }
  36. /// <inheritdoc/>
  37. public override void BoundsToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true)
  38. {
  39. // Frames are *Children* of a View, not SubViews. Thus View.BoundsToScreen will not work.
  40. // To get the screen-relative coordinates of a Frame, we need to know who
  41. // the Parent is
  42. var parentFrame = Parent?.Frame ?? Frame;
  43. rrow = row + parentFrame.Y;
  44. rcol = col + parentFrame.X;
  45. // We now have rcol/rrow in coordinates relative to our View's SuperView. If our View's SuperView has
  46. // a SuperView, keep going...
  47. Parent?.SuperView?.BoundsToScreen (rcol, rrow, out rcol, out rrow, clipped);
  48. }
  49. /// <inheritdoc/>
  50. public override Rect FrameToScreen ()
  51. {
  52. // Frames are *Children* of a View, not SubViews. Thus View.FrameToScreen will not work.
  53. // To get the screen-relative coordinates of a Frame, we need to know who
  54. // the Parent is
  55. var ret = Parent?.Frame ?? Frame;
  56. ret.Size = Frame.Size;
  57. ret.Location = Parent?.FrameToScreen ().Location ?? ret.Location;
  58. // We now have coordinates relative to our View. If our View's SuperView has
  59. // a SuperView, keep going...
  60. return ret;
  61. }
  62. /// <summary>
  63. /// Does nothing for Frame
  64. /// </summary>
  65. /// <returns></returns>
  66. public override bool OnDrawFrames () => false;
  67. /// <summary>
  68. /// Does nothing for Frame
  69. /// </summary>
  70. /// <returns></returns>
  71. public override bool OnRenderLineCanvas () => false;
  72. /// <summary>
  73. /// Frames only render to their Parent or Parent's SuperView's LineCanvas,
  74. /// so this always throws an <see cref="InvalidOperationException"/>.
  75. /// </summary>
  76. public override bool SuperViewRendersLineCanvas {
  77. get {
  78. return false;// throw new NotImplementedException ();
  79. }
  80. set {
  81. throw new NotImplementedException ();
  82. }
  83. }
  84. /// <summary>
  85. ///
  86. /// </summary>
  87. /// <param name="clipRect"></param>
  88. public virtual void OnDrawSubViews (Rect clipRect)
  89. {
  90. // TODO: Enable subviews of Frames (adornments).
  91. // if (Subviews == null) {
  92. // return;
  93. // }
  94. // foreach (var view in Subviews) {
  95. // // BUGBUG: v2 - shouldn't this be !view.LayoutNeeded? Why draw if layout is going to happen and we'll just draw again?
  96. // if (view.LayoutNeeded) {
  97. // view.LayoutSubviews ();
  98. // }
  99. // if ((view.Visible && !view.NeedDisplay.IsEmpty && view.Frame.Width > 0 && view.Frame.Height > 0) || view.ChildNeedsDisplay) {
  100. // view.Redraw (view.Bounds);
  101. // view.NeedDisplay = Rect.Empty;
  102. // // BUGBUG - v2 why does this need to be set to false?
  103. // // Shouldn't it be set when the subviews draw?
  104. // view.ChildNeedsDisplay = false;
  105. // }
  106. // }
  107. }
  108. /// <summary>
  109. /// Redraws the Frames that comprise the <see cref="Frame"/>.
  110. /// </summary>
  111. public override void OnDrawContent (Rect contentArea)
  112. {
  113. if (Thickness == Thickness.Empty) {
  114. return;
  115. }
  116. if (ColorScheme != null) {
  117. Driver.SetAttribute (GetNormalColor ());
  118. } else {
  119. if (Id == "Padding") {
  120. Driver.SetAttribute (new Attribute (Parent.ColorScheme.HotNormal.Background, Parent.ColorScheme.HotNormal.Foreground));
  121. } else {
  122. Driver.SetAttribute (Parent.GetNormalColor ());
  123. }
  124. }
  125. //Driver.SetAttribute (Colors.Error.Normal);
  126. var screenBounds = BoundsToScreen (Frame);
  127. // This just draws/clears the thickness, not the insides.
  128. Thickness.Draw (screenBounds, (string)(Data != null ? Data : string.Empty));
  129. //OnDrawSubviews (bounds);
  130. // TODO: v2 - this will eventually be two controls: "BorderView" and "Label" (for the title)
  131. // The border frame (and title) are drawn at the outermost edge of border;
  132. // For Border
  133. // ...thickness extends outward (border/title is always as far in as possible)
  134. var borderBounds = new Rect (
  135. screenBounds.X + Math.Max (0, Thickness.Left - 1),
  136. screenBounds.Y + Math.Max (0, Thickness.Top - 1),
  137. Math.Max (0, screenBounds.Width - Math.Max (0, Math.Max (0, Thickness.Left - 1) + Math.Max (0, Thickness.Right - 1))),
  138. Math.Max (0, screenBounds.Height - Math.Max (0, Math.Max (0, Thickness.Top - 1) + Math.Max (0, Thickness.Bottom - 1))));
  139. var topTitleLineY = borderBounds.Y;
  140. var titleY = borderBounds.Y;
  141. var titleBarsLength = 0; // the little vertical thingies
  142. var maxTitleWidth = Math.Min (Parent.Title.GetColumns (), Math.Min (screenBounds.Width - 4, borderBounds.Width - 4));
  143. var sideLineLength = borderBounds.Height;
  144. var canDrawBorder = borderBounds.Width > 0 && borderBounds.Height > 0;
  145. if (!string.IsNullOrEmpty (Parent?.Title)) {
  146. if (Thickness.Top == 2) {
  147. topTitleLineY = borderBounds.Y - 1;
  148. titleY = topTitleLineY + 1;
  149. titleBarsLength = 2;
  150. }
  151. // ┌────┐
  152. //┌┘View└
  153. //│
  154. if (Thickness.Top == 3) {
  155. topTitleLineY = borderBounds.Y - (Thickness.Top - 1);
  156. titleY = topTitleLineY + 1;
  157. titleBarsLength = 3;
  158. sideLineLength++;
  159. }
  160. // ┌────┐
  161. //┌┘View└
  162. //│
  163. if (Thickness.Top > 3) {
  164. topTitleLineY = borderBounds.Y - 2;
  165. titleY = topTitleLineY + 1;
  166. titleBarsLength = 3;
  167. sideLineLength++;
  168. }
  169. }
  170. if (Id == "Border" && canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) {
  171. var prevAttr = Driver.GetAttribute ();
  172. if (ColorScheme != null) {
  173. Driver.SetAttribute (HasFocus ? GetHotNormalColor () : GetNormalColor ());
  174. } else {
  175. Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ());
  176. }
  177. DrawTitle (new Rect (borderBounds.X, titleY, maxTitleWidth, 1), Parent?.Title);
  178. Driver.SetAttribute (prevAttr);
  179. }
  180. if (Id == "Border" && canDrawBorder && BorderStyle != LineStyle.None) {
  181. LineCanvas lc = Parent?.LineCanvas;
  182. var drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height > 1;
  183. var drawLeft = Thickness.Left > 0 && (Frame.Height > 1 || Thickness.Top == 0);
  184. var drawBottom = Thickness.Bottom > 0 && Frame.Width > 1;
  185. var drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0);
  186. var prevAttr = Driver.GetAttribute ();
  187. if (ColorScheme != null) {
  188. Driver.SetAttribute (GetNormalColor ());
  189. } else {
  190. Driver.SetAttribute (Parent.GetNormalColor ());
  191. }
  192. if (drawTop) {
  193. // ╔╡Title╞═════╗
  194. // ╔╡╞═════╗
  195. if (borderBounds.Width < 4 || string.IsNullOrEmpty (Parent?.Title)) {
  196. // ╔╡╞╗ should be ╔══╗
  197. lc.AddLine (new Point (borderBounds.Location.X, titleY), borderBounds.Width, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
  198. } else {
  199. // ┌────┐
  200. //┌┘View└
  201. //│
  202. if (Thickness.Top == 2) {
  203. lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
  204. }
  205. // ┌────┐
  206. //┌┘View└
  207. //│
  208. if (borderBounds.Width >= 4 && Thickness.Top > 2) {
  209. lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
  210. lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY + 2), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
  211. }
  212. // ╔╡Title╞═════╗
  213. // Add a short horiz line for ╔╡
  214. lc.AddLine (new Point (borderBounds.Location.X, titleY), 2, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
  215. // Add a vert line for ╔╡
  216. lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, Driver.GetAttribute ());
  217. // Add a vert line for ╞
  218. lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, Driver.GetAttribute ());
  219. // Add the right hand line for ╞═════╗
  220. lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, titleY), borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
  221. }
  222. }
  223. if (drawLeft) {
  224. lc.AddLine (new Point (borderBounds.Location.X, titleY), sideLineLength, Orientation.Vertical, BorderStyle, Driver.GetAttribute ());
  225. }
  226. if (drawBottom) {
  227. lc.AddLine (new Point (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), borderBounds.Width, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
  228. }
  229. if (drawRight) {
  230. lc.AddLine (new Point (borderBounds.X + borderBounds.Width - 1, titleY), sideLineLength, Orientation.Vertical, BorderStyle, Driver.GetAttribute ());
  231. }
  232. Driver.SetAttribute (prevAttr);
  233. // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler
  234. if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) {
  235. // Top
  236. var hruler = new Ruler () { Length = screenBounds.Width, Orientation = Orientation.Horizontal };
  237. if (drawTop) {
  238. hruler.Draw (new Point (screenBounds.X, screenBounds.Y));
  239. }
  240. // Redraw title
  241. if (drawTop && Id == "Border" && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) {
  242. prevAttr = Driver.GetAttribute ();
  243. if (ColorScheme != null) {
  244. Driver.SetAttribute (HasFocus ? GetHotNormalColor () : GetNormalColor ());
  245. } else {
  246. Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ());
  247. }
  248. DrawTitle (new Rect (borderBounds.X, titleY, Parent.Title.GetColumns (), 1), Parent?.Title);
  249. Driver.SetAttribute (prevAttr);
  250. }
  251. //Left
  252. var vruler = new Ruler () { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical };
  253. if (drawLeft) {
  254. vruler.Draw (new Point (screenBounds.X, screenBounds.Y + 1), 1);
  255. }
  256. // Bottom
  257. if (drawBottom) {
  258. hruler.Draw (new Point (screenBounds.X, screenBounds.Y + screenBounds.Height - 1));
  259. }
  260. // Right
  261. if (drawRight) {
  262. vruler.Draw (new Point (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1);
  263. }
  264. }
  265. }
  266. ClearNeedsDisplay ();
  267. }
  268. // TODO: v2 - Frame.BorderStyle is temporary - Eventually the border will be drawn by a "BorderView" that is a subview of the Frame.
  269. /// <summary>
  270. ///
  271. /// </summary>
  272. public new LineStyle BorderStyle { get; set; } = LineStyle.None;
  273. /// <summary>
  274. /// Defines the rectangle that the <see cref="Frame"/> will use to draw its content.
  275. /// </summary>
  276. public Thickness Thickness {
  277. get { return _thickness; }
  278. set {
  279. var prev = _thickness;
  280. _thickness = value;
  281. if (prev != _thickness) {
  282. Parent?.LayoutFrames ();
  283. OnThicknessChanged (prev);
  284. }
  285. }
  286. }
  287. /// <summary>
  288. /// Called whenever the <see cref="Thickness"/> property changes.
  289. /// </summary>
  290. public virtual void OnThicknessChanged (Thickness previousThickness)
  291. {
  292. ThicknessChanged?.Invoke (this, new ThicknessEventArgs () { Thickness = Thickness, PreviousThickness = previousThickness });
  293. }
  294. /// <summary>
  295. /// Fired whenever the <see cref="Thickness"/> property changes.
  296. /// </summary>
  297. public event EventHandler<ThicknessEventArgs> ThicknessChanged;
  298. /// <summary>
  299. /// Gets the rectangle that describes the inner area of the frame. The Location is always (0,0).
  300. /// </summary>
  301. public override Rect Bounds {
  302. get {
  303. return Thickness?.GetInside (new Rect (Point.Empty, Frame.Size)) ?? new Rect (Point.Empty, Frame.Size);
  304. }
  305. set {
  306. throw new InvalidOperationException ("It makes no sense to set Bounds of a Thickness.");
  307. }
  308. }
  309. /// <summary>
  310. /// Draws the title for a Window-style view.
  311. /// </summary>
  312. /// <param name="region">Screen relative region where the title will be drawn.</param>
  313. /// <param name="title">The title.</param>
  314. public void DrawTitle (Rect region, string title)
  315. {
  316. var width = region.Width;
  317. if (!string.IsNullOrEmpty (title)) {
  318. Driver.Move (region.X + 2, region.Y);
  319. //Driver.AddRune (' ');
  320. var str = title.EnumerateRunes ().Sum (r => Math.Max (r.GetColumns (), 1)) >= width
  321. ? TextFormatter.Format (title, width, false, false) [0] : title;
  322. Driver.AddStr (str);
  323. }
  324. }
  325. /// <summary>
  326. /// Draws a frame in the current view, clipped by the boundary of this view
  327. /// </summary>
  328. /// <param name="region">View-relative region for the frame to be drawn.</param>
  329. /// <param name="clear">If set to <see langword="true"/> it clear the region.</param>
  330. [ObsoleteAttribute ("This method is obsolete in v2. Use use LineCanvas or Frame instead.", false)]
  331. public void DrawFrame (Rect region, bool clear)
  332. {
  333. var savedClip = ClipToBounds ();
  334. var screenBounds = BoundsToScreen (region);
  335. if (clear) {
  336. Driver.FillRect (region);
  337. }
  338. var lc = new LineCanvas ();
  339. var drawTop = region.Width > 1 && region.Height > 1;
  340. var drawLeft = region.Width > 1 && region.Height > 1;
  341. var drawBottom = region.Width > 1 && region.Height > 1;
  342. var drawRight = region.Width > 1 && region.Height > 1;
  343. if (drawTop) {
  344. lc.AddLine (screenBounds.Location, screenBounds.Width, Orientation.Horizontal, BorderStyle);
  345. }
  346. if (drawLeft) {
  347. lc.AddLine (screenBounds.Location, screenBounds.Height, Orientation.Vertical, BorderStyle);
  348. }
  349. if (drawBottom) {
  350. lc.AddLine (new Point (screenBounds.X, screenBounds.Y + screenBounds.Height - 1), screenBounds.Width, Orientation.Horizontal, BorderStyle);
  351. }
  352. if (drawRight) {
  353. lc.AddLine (new Point (screenBounds.X + screenBounds.Width - 1, screenBounds.Y), screenBounds.Height, Orientation.Vertical, BorderStyle);
  354. }
  355. foreach (var p in lc.GetMap ()) {
  356. Driver.Move (p.Key.X, p.Key.Y);
  357. Driver.AddRune (p.Value);
  358. }
  359. lc.Clear ();
  360. // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler
  361. if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) {
  362. // Top
  363. var hruler = new Ruler () { Length = screenBounds.Width, Orientation = Orientation.Horizontal };
  364. if (drawTop) {
  365. hruler.Draw (new Point (screenBounds.X, screenBounds.Y));
  366. }
  367. //Left
  368. var vruler = new Ruler () { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical };
  369. if (drawLeft) {
  370. vruler.Draw (new Point (screenBounds.X, screenBounds.Y + 1), 1);
  371. }
  372. // Bottom
  373. if (drawBottom) {
  374. hruler.Draw (new Point (screenBounds.X, screenBounds.Y + screenBounds.Height - 1));
  375. }
  376. // Right
  377. if (drawRight) {
  378. vruler.Draw (new Point (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1);
  379. }
  380. }
  381. Driver.Clip = savedClip;
  382. }
  383. }
  384. }