Border.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. using System.Diagnostics;
  2. namespace Terminal.Gui.ViewBase;
  3. /// <summary>The Border for a <see cref="View"/>. Accessed via <see cref="View.Border"/></summary>
  4. /// <remarks>
  5. /// <para>
  6. /// Renders a border around the view with the <see cref="View.Title"/>. A border using <see cref="LineStyle"/>
  7. /// will be drawn on the sides of <see cref="Drawing.Thickness"/> that are greater than zero.
  8. /// </para>
  9. /// <para>
  10. /// The <see cref="View.Title"/> of <see cref="Adornment.Parent"/> will be drawn based on the value of
  11. /// <see cref="Drawing.Thickness.Top"/>:
  12. /// <example>
  13. /// // If Thickness.Top is 1:
  14. /// ┌┤1234├──┐
  15. /// │ │
  16. /// └────────┘
  17. /// // If Thickness.Top is 2:
  18. /// ┌────┐
  19. /// ┌┤1234├──┐
  20. /// │ │
  21. /// └────────┘
  22. /// If Thickness.Top is 3:
  23. /// ┌────┐
  24. /// ┌┤1234├──┐
  25. /// │└────┘ │
  26. /// │ │
  27. /// └────────┘
  28. /// </example>
  29. /// </para>
  30. /// <para>
  31. /// The Border provides keyboard and mouse support for moving and resizing the View. See <see cref="ViewArrangement"/>.
  32. /// </para>
  33. /// </remarks>
  34. public partial class Border : Adornment
  35. {
  36. private LineStyle? _lineStyle;
  37. /// <inheritdoc/>
  38. public Border ()
  39. { /* Do nothing; A parameter-less constructor is required to support all views unit tests. */
  40. }
  41. /// <inheritdoc/>
  42. public Border (View parent) : base (parent)
  43. {
  44. Parent = parent;
  45. CanFocus = false;
  46. TabStop = TabBehavior.TabGroup;
  47. Application.Mouse.GrabbingMouse += Application_GrabbingMouse;
  48. Application.Mouse.UnGrabbingMouse += Application_UnGrabbingMouse;
  49. ThicknessChanged += OnThicknessChanged;
  50. }
  51. // TODO: Move DrawIndicator out of Border and into View
  52. private void OnThicknessChanged (object? sender, EventArgs e)
  53. {
  54. if (IsInitialized)
  55. {
  56. ShowHideDrawIndicator ();
  57. }
  58. }
  59. private void ShowHideDrawIndicator ()
  60. {
  61. if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.DrawIndicator) && Thickness != Thickness.Empty)
  62. {
  63. if (DrawIndicator is null)
  64. {
  65. DrawIndicator = new ()
  66. {
  67. Id = "DrawIndicator",
  68. X = 1,
  69. Style = new SpinnerStyle.Dots2 (),
  70. SpinDelay = 0,
  71. Visible = false
  72. };
  73. Add (DrawIndicator);
  74. }
  75. }
  76. else if (DrawIndicator is { })
  77. {
  78. Remove (DrawIndicator);
  79. DrawIndicator!.Dispose ();
  80. DrawIndicator = null;
  81. }
  82. }
  83. internal void AdvanceDrawIndicator ()
  84. {
  85. if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.DrawIndicator) && DrawIndicator is { })
  86. {
  87. DrawIndicator.AdvanceAnimation (false);
  88. DrawIndicator.Render ();
  89. }
  90. }
  91. #if SUBVIEW_BASED_BORDER
  92. private Line _left;
  93. /// <summary>
  94. /// The close button for the border. Set to <see cref="View.Visible"/>, to <see langword="true"/> to enable.
  95. /// </summary>
  96. public Button CloseButton { get; internal set; }
  97. #endif
  98. /// <inheritdoc/>
  99. public override void BeginInit ()
  100. {
  101. base.BeginInit ();
  102. if (Parent is null)
  103. {
  104. return;
  105. }
  106. ShowHideDrawIndicator ();
  107. HighlightStates |= (Parent.Arrangement != ViewArrangement.Fixed ? MouseState.Pressed : MouseState.None);
  108. #if SUBVIEW_BASED_BORDER
  109. if (Parent is { })
  110. {
  111. // Left
  112. _left = new ()
  113. {
  114. Orientation = Orientation.Vertical,
  115. };
  116. Add (_left);
  117. CloseButton = new Button ()
  118. {
  119. Text = "X",
  120. CanFocus = true,
  121. Visible = false,
  122. };
  123. CloseButton.Accept += (s, e) =>
  124. {
  125. e.Handled = Parent.InvokeCommand (Command.QuitToplevel) == true;
  126. };
  127. Add (CloseButton);
  128. LayoutStarted += OnLayoutStarted;
  129. }
  130. #endif
  131. }
  132. #if SUBVIEW_BASED_BORDER
  133. private void OnLayoutStarted (object sender, LayoutEventArgs e)
  134. {
  135. _left.Border!.LineStyle = LineStyle;
  136. _left.X = Thickness.Left - 1;
  137. _left.Y = Thickness.Top - 1;
  138. _left.Width = 1;
  139. _left.Height = Height;
  140. CloseButton.X = Pos.AnchorEnd (Thickness.Right / 2 + 1) -
  141. (Pos.Right (CloseButton) -
  142. Pos.Left (CloseButton));
  143. CloseButton.Y = 0;
  144. }
  145. #endif
  146. internal Rectangle GetBorderRectangle ()
  147. {
  148. Rectangle screenRect = ViewportToScreen (Viewport);
  149. return new (
  150. screenRect.X + Math.Max (0, Thickness.Left - 1),
  151. screenRect.Y + Math.Max (0, Thickness.Top - 1),
  152. Math.Max (
  153. 0,
  154. screenRect.Width
  155. - Math.Max (
  156. 0,
  157. Math.Max (0, Thickness.Left - 1)
  158. + Math.Max (0, Thickness.Right - 1)
  159. )
  160. ),
  161. Math.Max (
  162. 0,
  163. screenRect.Height
  164. - Math.Max (
  165. 0,
  166. Math.Max (0, Thickness.Top - 1)
  167. + Math.Max (0, Thickness.Bottom - 1)
  168. )
  169. )
  170. );
  171. }
  172. // TODO: Make LineStyle nullable https://github.com/gui-cs/Terminal.Gui/issues/4021
  173. /// <summary>
  174. /// Sets the style of the border by changing the <see cref="Thickness"/>. This is a helper API for setting the
  175. /// <see cref="Thickness"/> to <c>(1,1,1,1)</c> and setting the line style of the views that comprise the border. If
  176. /// set to <see cref="LineStyle.None"/> no border will be drawn.
  177. /// </summary>
  178. public LineStyle LineStyle
  179. {
  180. get
  181. {
  182. if (_lineStyle.HasValue)
  183. {
  184. return _lineStyle.Value;
  185. }
  186. // TODO: Make Border.LineStyle inherit from the SuperView hierarchy
  187. // TODO: Right now, Window and FrameView use CM to set BorderStyle, which negates
  188. // TODO: all this.
  189. return Parent?.SuperView?.BorderStyle ?? LineStyle.None;
  190. }
  191. set => _lineStyle = value;
  192. }
  193. private BorderSettings _settings = BorderSettings.Title;
  194. /// <summary>
  195. /// Gets or sets the settings for the border.
  196. /// </summary>
  197. public BorderSettings Settings
  198. {
  199. get => _settings;
  200. set
  201. {
  202. if (value == _settings)
  203. {
  204. return;
  205. }
  206. _settings = value;
  207. Parent?.SetNeedsDraw ();
  208. }
  209. }
  210. /// <inheritdoc/>
  211. protected override bool OnDrawingContent ()
  212. {
  213. if (Thickness == Thickness.Empty)
  214. {
  215. return true;
  216. }
  217. Rectangle screenBounds = ViewportToScreen (Viewport);
  218. // TODO: v2 - this will eventually be two controls: "BorderView" and "Label" (for the title)
  219. // The border adornment (and title) are drawn at the outermost edge of border;
  220. // For Border
  221. // ...thickness extends outward (border/title is always as far in as possible)
  222. // PERF: How about a call to Rectangle.Offset?
  223. Rectangle borderBounds = GetBorderRectangle ();
  224. int topTitleLineY = borderBounds.Y;
  225. int titleY = borderBounds.Y;
  226. var titleBarsLength = 0; // the little vertical thingies
  227. int maxTitleWidth = Math.Max (
  228. 0,
  229. Math.Min (
  230. Parent?.TitleTextFormatter.FormatAndGetSize ().Width ?? 0,
  231. Math.Min (screenBounds.Width - 4, borderBounds.Width - 4)
  232. )
  233. );
  234. if (Parent is { })
  235. {
  236. Parent.TitleTextFormatter.ConstrainToSize = new (maxTitleWidth, 1);
  237. }
  238. int sideLineLength = borderBounds.Height;
  239. bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 };
  240. LineStyle lineStyle = LineStyle;
  241. if (Settings.FastHasFlags (BorderSettings.Title))
  242. {
  243. if (Thickness.Top == 2)
  244. {
  245. topTitleLineY = borderBounds.Y - 1;
  246. titleY = topTitleLineY + 1;
  247. titleBarsLength = 2;
  248. }
  249. // ┌────┐
  250. //┌┘View└
  251. //│
  252. if (Thickness.Top == 3)
  253. {
  254. topTitleLineY = borderBounds.Y - (Thickness.Top - 1);
  255. titleY = topTitleLineY + 1;
  256. titleBarsLength = 3;
  257. sideLineLength++;
  258. }
  259. // ┌────┐
  260. //┌┘View└
  261. //│
  262. if (Thickness.Top > 3)
  263. {
  264. topTitleLineY = borderBounds.Y - 2;
  265. titleY = topTitleLineY + 1;
  266. titleBarsLength = 3;
  267. sideLineLength++;
  268. }
  269. }
  270. if (Parent is { }
  271. && canDrawBorder
  272. && Thickness.Top > 0
  273. && maxTitleWidth > 0
  274. && Settings.FastHasFlags (BorderSettings.Title)
  275. && !string.IsNullOrEmpty (Parent?.Title))
  276. {
  277. Rectangle titleRect = new (borderBounds.X + 2, titleY, maxTitleWidth, 1);
  278. Parent.TitleTextFormatter.Draw (
  279. titleRect,
  280. GetAttributeForRole (Parent.HasFocus ? VisualRole.Focus : VisualRole.Normal),
  281. GetAttributeForRole (Parent.HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal));
  282. Parent?.LineCanvas.Exclude (new (titleRect));
  283. }
  284. if (canDrawBorder && LineStyle != LineStyle.None)
  285. {
  286. LineCanvas? lc = Parent?.LineCanvas;
  287. bool drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height >= 1;
  288. bool drawLeft = Thickness.Left > 0 && (Frame.Height > 1 || Thickness.Top == 0);
  289. bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1 && Frame.Height > 1;
  290. bool drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0);
  291. //Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default;
  292. Attribute normalAttribute = GetAttributeForRole (VisualRole.Normal);
  293. if (MouseState.HasFlag (MouseState.Pressed))
  294. {
  295. normalAttribute = GetAttributeForRole (VisualRole.Highlight);
  296. }
  297. SetAttribute (normalAttribute);
  298. if (drawTop)
  299. {
  300. // ╔╡Title╞═════╗
  301. // ╔╡╞═════╗
  302. if (borderBounds.Width < 4 || !Settings.FastHasFlags (BorderSettings.Title) || string.IsNullOrEmpty (Parent?.Title))
  303. {
  304. // ╔╡╞╗ should be ╔══╗
  305. lc?.AddLine (
  306. new (borderBounds.Location.X, titleY),
  307. borderBounds.Width,
  308. Orientation.Horizontal,
  309. lineStyle,
  310. normalAttribute
  311. );
  312. }
  313. else
  314. {
  315. // ┌────┐
  316. //┌┘View└
  317. //│
  318. if (Thickness.Top == 2)
  319. {
  320. lc?.AddLine (
  321. new (borderBounds.X + 1, topTitleLineY),
  322. Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  323. Orientation.Horizontal,
  324. lineStyle,
  325. normalAttribute
  326. );
  327. }
  328. // ┌────┐
  329. //┌┘View└
  330. //│
  331. if (borderBounds.Width >= 4 && Thickness.Top > 2)
  332. {
  333. lc?.AddLine (
  334. new (borderBounds.X + 1, topTitleLineY),
  335. Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  336. Orientation.Horizontal,
  337. lineStyle,
  338. normalAttribute
  339. );
  340. lc?.AddLine (
  341. new (borderBounds.X + 1, topTitleLineY + 2),
  342. Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  343. Orientation.Horizontal,
  344. lineStyle,
  345. normalAttribute
  346. );
  347. }
  348. // ╔╡Title╞═════╗
  349. // Add a short horiz line for ╔╡
  350. lc?.AddLine (
  351. new (borderBounds.Location.X, titleY),
  352. 2,
  353. Orientation.Horizontal,
  354. lineStyle,
  355. normalAttribute
  356. );
  357. // Add a vert line for ╔╡
  358. lc?.AddLine (
  359. new (borderBounds.X + 1, topTitleLineY),
  360. titleBarsLength,
  361. Orientation.Vertical,
  362. LineStyle.Single,
  363. normalAttribute
  364. );
  365. // Add a vert line for ╞
  366. lc?.AddLine (
  367. new (
  368. borderBounds.X
  369. + 1
  370. + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2)
  371. - 1,
  372. topTitleLineY
  373. ),
  374. titleBarsLength,
  375. Orientation.Vertical,
  376. LineStyle.Single,
  377. normalAttribute
  378. );
  379. // Add the right hand line for ╞═════╗
  380. lc?.AddLine (
  381. new (
  382. borderBounds.X
  383. + 1
  384. + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2)
  385. - 1,
  386. titleY
  387. ),
  388. borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2),
  389. Orientation.Horizontal,
  390. lineStyle,
  391. normalAttribute
  392. );
  393. }
  394. }
  395. #if !SUBVIEW_BASED_BORDER
  396. if (drawLeft)
  397. {
  398. lc?.AddLine (
  399. new (borderBounds.Location.X, titleY),
  400. sideLineLength,
  401. Orientation.Vertical,
  402. lineStyle,
  403. normalAttribute
  404. );
  405. }
  406. #endif
  407. if (drawBottom)
  408. {
  409. lc?.AddLine (
  410. new (borderBounds.X, borderBounds.Y + borderBounds.Height - 1),
  411. borderBounds.Width,
  412. Orientation.Horizontal,
  413. lineStyle,
  414. normalAttribute
  415. );
  416. }
  417. if (drawRight)
  418. {
  419. lc?.AddLine (
  420. new (borderBounds.X + borderBounds.Width - 1, titleY),
  421. sideLineLength,
  422. Orientation.Vertical,
  423. lineStyle,
  424. normalAttribute
  425. );
  426. }
  427. // SetAttribute (prevAttr);
  428. // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler
  429. if (Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler))
  430. {
  431. // Top
  432. var hruler = new Ruler { Length = screenBounds.Width, Orientation = Orientation.Horizontal };
  433. if (drawTop)
  434. {
  435. hruler.Draw (new (screenBounds.X, screenBounds.Y));
  436. }
  437. // Redraw title
  438. if (drawTop && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title))
  439. {
  440. Parent!.TitleTextFormatter.Draw (
  441. new (borderBounds.X + 2, titleY, maxTitleWidth, 1),
  442. Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal),
  443. Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal));
  444. }
  445. //Left
  446. var vruler = new Ruler { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical };
  447. if (drawLeft)
  448. {
  449. vruler.Draw (new (screenBounds.X, screenBounds.Y + 1), 1);
  450. }
  451. // Bottom
  452. if (drawBottom)
  453. {
  454. hruler.Draw (new (screenBounds.X, screenBounds.Y + screenBounds.Height - 1));
  455. }
  456. // Right
  457. if (drawRight)
  458. {
  459. vruler.Draw (new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1);
  460. }
  461. }
  462. // TODO: This should not be done on each draw?
  463. if (Settings.FastHasFlags (BorderSettings.Gradient))
  464. {
  465. SetupGradientLineCanvas (lc!, screenBounds);
  466. }
  467. else
  468. {
  469. lc!.Fill = null;
  470. }
  471. }
  472. return true;
  473. ;
  474. }
  475. /// <summary>
  476. /// Gets the subview used to render <see cref="ViewDiagnosticFlags.DrawIndicator"/>.
  477. /// </summary>
  478. public SpinnerView? DrawIndicator { get; private set; }
  479. private void SetupGradientLineCanvas (LineCanvas lc, Rectangle rect)
  480. {
  481. GetAppealingGradientColors (out List<Color> stops, out List<int> steps);
  482. var g = new Gradient (stops, steps);
  483. var fore = new GradientFill (rect, g, GradientDirection.Diagonal);
  484. var back = new SolidFill (GetAttributeForRole (VisualRole.Normal).Background);
  485. lc.Fill = new (fore, back);
  486. }
  487. private static void GetAppealingGradientColors (out List<Color> stops, out List<int> steps)
  488. {
  489. // Define the colors of the gradient stops with more appealing colors
  490. stops =
  491. [
  492. new (0, 128, 255), // Bright Blue
  493. new (0, 255, 128), // Bright Green
  494. new (255, 255), // Bright Yellow
  495. new (255, 128), // Bright Orange
  496. new (255, 0, 128)
  497. ];
  498. // Define the number of steps between each color for smoother transitions
  499. // If we pass only a single value then it will assume equal steps between all pairs
  500. steps = [15];
  501. }
  502. }