Border.cs 20 KB

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