Border.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. namespace Terminal.Gui;
  2. /// <summary>The Border for a <see cref="View"/>.</summary>
  3. /// <remarks>
  4. /// <para>
  5. /// Renders a border around the view with the <see cref="View.Title"/>. A border using <see cref="LineStyle"/>
  6. /// will be drawn on the sides of <see cref="Thickness"/> that are greater than zero.
  7. /// </para>
  8. /// <para>
  9. /// The <see cref="View.Title"/> of <see cref="Adornment.Parent"/> will be drawn based on the value of
  10. /// <see cref="Thickness.Top"/>:
  11. /// </para>
  12. /// <para>
  13. /// If <c>1</c>:
  14. /// <code>
  15. /// ┌┤1234├──┐
  16. /// │ │
  17. /// └────────┘
  18. /// </code>
  19. /// </para>
  20. /// <para>
  21. /// If <c>2</c>:
  22. /// <code>
  23. /// ┌────┐
  24. /// ┌┤1234├──┐
  25. /// │ │
  26. /// └────────┘
  27. /// </code>
  28. /// </para>
  29. /// <para>
  30. /// If <c>3</c>:
  31. /// <code>
  32. /// ┌────┐
  33. /// ┌┤1234├──┐
  34. /// │└────┘ │
  35. /// │ │
  36. /// └────────┘
  37. /// </code>
  38. /// </para>
  39. /// <para/>
  40. /// <para>See the <see cref="Adornment"/> class.</para>
  41. /// </remarks>
  42. public class Border : Adornment
  43. {
  44. private LineStyle? _lineStyle;
  45. /// <inheritdoc/>
  46. public Border ()
  47. { /* Do nothing; A parameter-less constructor is required to support all views unit tests. */
  48. }
  49. /// <inheritdoc/>
  50. public Border (View parent) : base (parent)
  51. {
  52. /* Do nothing; View.CreateAdornment requires a constructor that takes a parent */
  53. Parent = parent;
  54. Application.GrabbingMouse += Application_GrabbingMouse;
  55. Application.UnGrabbingMouse += Application_UnGrabbingMouse;
  56. EnablingHighlight += Border_EnablingHighlight;
  57. DisablingHighlight += Border_DisablingHighlight;
  58. }
  59. #if SUBVIEW_BASED_BORDER
  60. private Line _left;
  61. /// <summary>
  62. /// The close button for the border. Set to <see cref="View.Visible"/>, to <see langword="true"/> to enable.
  63. /// </summary>
  64. public Button CloseButton { get; internal set; }
  65. #endif
  66. /// <inheritdoc/>
  67. public override void BeginInit ()
  68. {
  69. base.BeginInit ();
  70. #if SUBVIEW_BASED_BORDER
  71. if (Parent is { })
  72. {
  73. // Left
  74. _left = new ()
  75. {
  76. Orientation = Orientation.Vertical,
  77. };
  78. Add (_left);
  79. CloseButton = new Button ()
  80. {
  81. Text = "X",
  82. CanFocus = true,
  83. Visible = false,
  84. };
  85. CloseButton.Accept += (s, e) =>
  86. {
  87. e.Cancel = Parent.InvokeCommand (Command.QuitToplevel) == true;
  88. };
  89. Add (CloseButton);
  90. LayoutStarted += OnLayoutStarted;
  91. }
  92. #endif
  93. }
  94. #if SUBVIEW_BASED_BORDER
  95. private void OnLayoutStarted (object sender, LayoutEventArgs e)
  96. {
  97. _left.Border.LineStyle = LineStyle;
  98. _left.X = Thickness.Left - 1;
  99. _left.Y = Thickness.Top - 1;
  100. _left.Width = 1;
  101. _left.Height = Height;
  102. CloseButton.X = Pos.AnchorEnd (Thickness.Right / 2 + 1) -
  103. (Pos.Right (CloseButton) -
  104. Pos.Left (CloseButton));
  105. CloseButton.Y = 0;
  106. }
  107. #endif
  108. /// <summary>
  109. /// The color scheme for the Border. If set to <see langword="null"/>, gets the <see cref="Adornment.Parent"/>
  110. /// scheme. color scheme.
  111. /// </summary>
  112. public override ColorScheme ColorScheme
  113. {
  114. get
  115. {
  116. if (base.ColorScheme is { })
  117. {
  118. return base.ColorScheme;
  119. }
  120. return Parent?.ColorScheme;
  121. }
  122. set
  123. {
  124. base.ColorScheme = value;
  125. Parent?.SetNeedsDisplay ();
  126. }
  127. }
  128. Rectangle GetBorderBounds (Rectangle screenBounds)
  129. {
  130. return new (
  131. screenBounds.X + Math.Max (0, Thickness.Left - 1),
  132. screenBounds.Y + Math.Max (0, Thickness.Top - 1),
  133. Math.Max (
  134. 0,
  135. screenBounds.Width
  136. - Math.Max (
  137. 0,
  138. Math.Max (0, Thickness.Left - 1)
  139. + Math.Max (0, Thickness.Right - 1)
  140. )
  141. ),
  142. Math.Max (
  143. 0,
  144. screenBounds.Height
  145. - Math.Max (
  146. 0,
  147. Math.Max (0, Thickness.Top - 1)
  148. + Math.Max (0, Thickness.Bottom - 1)
  149. )
  150. )
  151. );
  152. }
  153. /// <summary>
  154. /// Sets the style of the border by changing the <see cref="Thickness"/>. This is a helper API for setting the
  155. /// <see cref="Thickness"/> to <c>(1,1,1,1)</c> and setting the line style of the views that comprise the border. If
  156. /// set to <see cref="LineStyle.None"/> no border will be drawn.
  157. /// </summary>
  158. public LineStyle LineStyle
  159. {
  160. get
  161. {
  162. if (_lineStyle.HasValue)
  163. {
  164. return _lineStyle.Value;
  165. }
  166. // TODO: Make Border.LineStyle inherit from the SuperView hierarchy
  167. // TODO: Right now, Window and FrameView use CM to set BorderStyle, which negates
  168. // TODO: all this.
  169. return Parent.SuperView?.BorderStyle ?? LineStyle.None;
  170. }
  171. set => _lineStyle = value;
  172. }
  173. #region Mouse Support
  174. private LineStyle _savedHighlightLineStyle;
  175. private void Border_EnablingHighlight (object sender, System.ComponentModel.CancelEventArgs e)
  176. {
  177. _savedHighlightLineStyle = Parent?.BorderStyle ?? LineStyle;
  178. LineStyle = LineStyle.Heavy;
  179. Parent?.SetNeedsDisplay ();
  180. e.Cancel = true;
  181. }
  182. private void Border_DisablingHighlight (object sender, System.ComponentModel.CancelEventArgs e)
  183. {
  184. LineStyle = _savedHighlightLineStyle;
  185. Parent?.SetNeedsDisplay ();
  186. e.Cancel = true;
  187. }
  188. private Point? _dragPosition;
  189. private Point _startGrabPoint;
  190. /// <inheritdoc />
  191. protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
  192. {
  193. if (base.OnMouseEvent (mouseEvent))
  194. {
  195. return true;
  196. }
  197. if (!Parent.CanFocus)
  198. {
  199. return false;
  200. }
  201. if (!Parent.Arrangement.HasFlag (ViewArrangement.Movable))
  202. {
  203. return false;
  204. }
  205. // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/3312
  206. if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  207. {
  208. Parent.SetFocus ();
  209. Application.BringOverlappedTopToFront ();
  210. // Only start grabbing if the user clicks in the Thickness area
  211. // Adornment.Contains takes Parent SuperView=relative coords.
  212. if (Contains (mouseEvent.X + Parent.Frame.X + Frame.X, mouseEvent.Y + Parent.Frame.Y + Frame.Y))
  213. {
  214. // Set the start grab point to the Frame coords
  215. _startGrabPoint = new (mouseEvent.X + Frame.X, mouseEvent.Y + Frame.Y);
  216. _dragPosition = new (mouseEvent.X, mouseEvent.Y);
  217. Application.GrabMouse (this);
  218. EnableHighlight ();
  219. }
  220. return true;
  221. }
  222. if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
  223. {
  224. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  225. {
  226. if (Parent.SuperView is null)
  227. {
  228. // Redraw the entire app window.
  229. Application.Top.SetNeedsDisplay ();
  230. }
  231. else
  232. {
  233. Parent.SuperView.SetNeedsDisplay ();
  234. }
  235. _dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  236. Point parentLoc = Parent.SuperView?.ScreenToBounds (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y) ?? mouseEvent.ScreenPosition;
  237. GetLocationEnsuringFullVisibility (
  238. Parent,
  239. parentLoc.X - _startGrabPoint.X,
  240. parentLoc.Y - _startGrabPoint.Y,
  241. out int nx,
  242. out int ny,
  243. out _
  244. );
  245. Parent.X = nx;
  246. Parent.Y = ny;
  247. return true;
  248. }
  249. }
  250. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue)
  251. {
  252. _dragPosition = null;
  253. Application.UngrabMouse ();
  254. DisableHighlight();
  255. return true;
  256. }
  257. return false;
  258. }
  259. /// <inheritdoc/>
  260. protected override void Dispose (bool disposing)
  261. {
  262. Application.GrabbingMouse -= Application_GrabbingMouse;
  263. Application.UnGrabbingMouse -= Application_UnGrabbingMouse;
  264. _dragPosition = null;
  265. base.Dispose (disposing);
  266. }
  267. private void Application_GrabbingMouse (object sender, GrabMouseEventArgs e)
  268. {
  269. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  270. {
  271. e.Cancel = true;
  272. }
  273. }
  274. private void Application_UnGrabbingMouse (object sender, GrabMouseEventArgs e)
  275. {
  276. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  277. {
  278. e.Cancel = true;
  279. }
  280. }
  281. #endregion Mouse Support
  282. /// <inheritdoc/>
  283. public override void OnDrawContent (Rectangle contentArea)
  284. {
  285. base.OnDrawContent (contentArea);
  286. if (Thickness == Thickness.Empty)
  287. {
  288. return;
  289. }
  290. //Driver.SetAttribute (Colors.ColorSchemes ["Error"].Normal);
  291. Rectangle screenBounds = BoundsToScreen (contentArea);
  292. //OnDrawSubviews (bounds);
  293. // TODO: v2 - this will eventually be two controls: "BorderView" and "Label" (for the title)
  294. // The border adornment (and title) are drawn at the outermost edge of border;
  295. // For Border
  296. // ...thickness extends outward (border/title is always as far in as possible)
  297. // PERF: How about a call to Rectangle.Offset?
  298. var borderBounds = GetBorderBounds (screenBounds);
  299. int topTitleLineY = borderBounds.Y;
  300. int titleY = borderBounds.Y;
  301. var titleBarsLength = 0; // the little vertical thingies
  302. int maxTitleWidth = Math.Max (
  303. 0,
  304. Math.Min (
  305. Parent.TitleTextFormatter.FormatAndGetSize ().Width,
  306. Math.Min (screenBounds.Width - 4, borderBounds.Width - 4)
  307. )
  308. );
  309. Parent.TitleTextFormatter.Size = new (maxTitleWidth, 1);
  310. int sideLineLength = borderBounds.Height;
  311. bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 };
  312. if (!string.IsNullOrEmpty (Parent?.Title))
  313. {
  314. if (Thickness.Top == 2)
  315. {
  316. topTitleLineY = borderBounds.Y - 1;
  317. titleY = topTitleLineY + 1;
  318. titleBarsLength = 2;
  319. }
  320. // ┌────┐
  321. //┌┘View└
  322. //│
  323. if (Thickness.Top == 3)
  324. {
  325. topTitleLineY = borderBounds.Y - (Thickness.Top - 1);
  326. titleY = topTitleLineY + 1;
  327. titleBarsLength = 3;
  328. sideLineLength++;
  329. }
  330. // ┌────┐
  331. //┌┘View└
  332. //│
  333. if (Thickness.Top > 3)
  334. {
  335. topTitleLineY = borderBounds.Y - 2;
  336. titleY = topTitleLineY + 1;
  337. titleBarsLength = 3;
  338. sideLineLength++;
  339. }
  340. }
  341. if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title))
  342. {
  343. Parent.TitleTextFormatter.Draw (
  344. new (borderBounds.X + 2, titleY, maxTitleWidth, 1),
  345. Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor (),
  346. Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetHotNormalColor ());
  347. }
  348. if (canDrawBorder && LineStyle != LineStyle.None)
  349. {
  350. LineCanvas lc = Parent?.LineCanvas;
  351. bool drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height > 1;
  352. bool drawLeft = Thickness.Left > 0 && (Frame.Height > 1 || Thickness.Top == 0);
  353. bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1;
  354. bool drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0);
  355. Attribute prevAttr = Driver.GetAttribute ();
  356. if (ColorScheme is { })
  357. {
  358. Driver.SetAttribute (GetNormalColor ());
  359. }
  360. else
  361. {
  362. Driver.SetAttribute (Parent.GetNormalColor ());
  363. }
  364. if (drawTop)
  365. {
  366. // ╔╡Title╞═════╗
  367. // ╔╡╞═════╗
  368. if (borderBounds.Width < 4 || string.IsNullOrEmpty (Parent?.Title))
  369. {
  370. // ╔╡╞╗ should be ╔══╗
  371. lc.AddLine (
  372. new (borderBounds.Location.X, titleY),
  373. borderBounds.Width,
  374. Orientation.Horizontal,
  375. LineStyle,
  376. Driver.GetAttribute ()
  377. );
  378. }
  379. else
  380. {
  381. // ┌────┐
  382. //┌┘View└
  383. //│
  384. if (Thickness.Top == 2)
  385. {
  386. lc.AddLine (
  387. new (borderBounds.X + 1, topTitleLineY),
  388. Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  389. Orientation.Horizontal,
  390. LineStyle,
  391. Driver.GetAttribute ()
  392. );
  393. }
  394. // ┌────┐
  395. //┌┘View└
  396. //│
  397. if (borderBounds.Width >= 4 && Thickness.Top > 2)
  398. {
  399. lc.AddLine (
  400. new (borderBounds.X + 1, topTitleLineY),
  401. Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  402. Orientation.Horizontal,
  403. LineStyle,
  404. Driver.GetAttribute ()
  405. );
  406. lc.AddLine (
  407. new (borderBounds.X + 1, topTitleLineY + 2),
  408. Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  409. Orientation.Horizontal,
  410. LineStyle,
  411. Driver.GetAttribute ()
  412. );
  413. }
  414. // ╔╡Title╞═════╗
  415. // Add a short horiz line for ╔╡
  416. lc.AddLine (
  417. new (borderBounds.Location.X, titleY),
  418. 2,
  419. Orientation.Horizontal,
  420. LineStyle,
  421. Driver.GetAttribute ()
  422. );
  423. // Add a vert line for ╔╡
  424. lc.AddLine (
  425. new (borderBounds.X + 1, topTitleLineY),
  426. titleBarsLength,
  427. Orientation.Vertical,
  428. LineStyle.Single,
  429. Driver.GetAttribute ()
  430. );
  431. // Add a vert line for ╞
  432. lc.AddLine (
  433. new (
  434. borderBounds.X
  435. + 1
  436. + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2)
  437. - 1,
  438. topTitleLineY
  439. ),
  440. titleBarsLength,
  441. Orientation.Vertical,
  442. LineStyle.Single,
  443. Driver.GetAttribute ()
  444. );
  445. // Add the right hand line for ╞═════╗
  446. lc.AddLine (
  447. new (
  448. borderBounds.X
  449. + 1
  450. + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2)
  451. - 1,
  452. titleY
  453. ),
  454. borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  455. Orientation.Horizontal,
  456. LineStyle,
  457. Driver.GetAttribute ()
  458. );
  459. }
  460. }
  461. #if !SUBVIEW_BASED_BORDER
  462. if (drawLeft)
  463. {
  464. lc.AddLine (
  465. new (borderBounds.Location.X, titleY),
  466. sideLineLength,
  467. Orientation.Vertical,
  468. LineStyle,
  469. Driver.GetAttribute ()
  470. );
  471. }
  472. #endif
  473. if (drawBottom)
  474. {
  475. lc.AddLine (
  476. new (borderBounds.X, borderBounds.Y + borderBounds.Height - 1),
  477. borderBounds.Width,
  478. Orientation.Horizontal,
  479. LineStyle,
  480. Driver.GetAttribute ()
  481. );
  482. }
  483. if (drawRight)
  484. {
  485. lc.AddLine (
  486. new (borderBounds.X + borderBounds.Width - 1, titleY),
  487. sideLineLength,
  488. Orientation.Vertical,
  489. LineStyle,
  490. Driver.GetAttribute ()
  491. );
  492. }
  493. Driver.SetAttribute (prevAttr);
  494. // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler
  495. if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler))
  496. {
  497. // Top
  498. var hruler = new Ruler { Length = screenBounds.Width, Orientation = Orientation.Horizontal };
  499. if (drawTop)
  500. {
  501. hruler.Draw (new (screenBounds.X, screenBounds.Y));
  502. }
  503. // Redraw title
  504. if (drawTop && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title))
  505. {
  506. Parent.TitleTextFormatter.Draw (
  507. new (borderBounds.X + 2, titleY, maxTitleWidth, 1),
  508. Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor (),
  509. Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor ());
  510. }
  511. //Left
  512. var vruler = new Ruler { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical };
  513. if (drawLeft)
  514. {
  515. vruler.Draw (new (screenBounds.X, screenBounds.Y + 1), 1);
  516. }
  517. // Bottom
  518. if (drawBottom)
  519. {
  520. hruler.Draw (new (screenBounds.X, screenBounds.Y + screenBounds.Height - 1));
  521. }
  522. // Right
  523. if (drawRight)
  524. {
  525. vruler.Draw (new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1);
  526. }
  527. }
  528. }
  529. //base.OnDrawContent (contentArea);
  530. }
  531. }