Border.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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. set => _lineStyle = value;
  194. }
  195. private BorderSettings _settings = BorderSettings.Title;
  196. /// <summary>
  197. /// Gets or sets the settings for the border.
  198. /// </summary>
  199. public BorderSettings Settings
  200. {
  201. get => _settings;
  202. set
  203. {
  204. if (value == _settings)
  205. {
  206. return;
  207. }
  208. _settings = value;
  209. Parent?.SetNeedsDraw ();
  210. }
  211. }
  212. /// <inheritdoc/>
  213. protected override bool OnDrawingContent (DrawContext? context)
  214. {
  215. if (Thickness == Thickness.Empty)
  216. {
  217. return true;
  218. }
  219. Rectangle screenBounds = ViewportToScreen (Viewport);
  220. // TODO: v2 - this will eventually be two controls: "BorderView" and "Label" (for the title)
  221. // The border adornment (and title) are drawn at the outermost edge of border;
  222. // For Border
  223. // ...thickness extends outward (border/title is always as far in as possible)
  224. // PERF: How about a call to Rectangle.Offset?
  225. Rectangle borderBounds = GetBorderRectangle ();
  226. int topTitleLineY = borderBounds.Y;
  227. int titleY = borderBounds.Y;
  228. var titleBarsLength = 0; // the little vertical thingies
  229. int maxTitleWidth = Math.Max (
  230. 0,
  231. Math.Min (
  232. Parent?.TitleTextFormatter.FormatAndGetSize ().Width ?? 0,
  233. Math.Min (screenBounds.Width - 4, borderBounds.Width - 4)
  234. )
  235. );
  236. if (Parent is { })
  237. {
  238. Parent.TitleTextFormatter.ConstrainToSize = new (maxTitleWidth, 1);
  239. }
  240. int sideLineLength = borderBounds.Height;
  241. bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 };
  242. LineStyle lineStyle = LineStyle;
  243. if (Settings.FastHasFlags (BorderSettings.Title))
  244. {
  245. if (Thickness.Top == 2)
  246. {
  247. topTitleLineY = borderBounds.Y - 1;
  248. titleY = topTitleLineY + 1;
  249. titleBarsLength = 2;
  250. }
  251. // ┌────┐
  252. //┌┘View└
  253. //│
  254. if (Thickness.Top == 3)
  255. {
  256. topTitleLineY = borderBounds.Y - (Thickness.Top - 1);
  257. titleY = topTitleLineY + 1;
  258. titleBarsLength = 3;
  259. sideLineLength++;
  260. }
  261. // ┌────┐
  262. //┌┘View└
  263. //│
  264. if (Thickness.Top > 3)
  265. {
  266. topTitleLineY = borderBounds.Y - 2;
  267. titleY = topTitleLineY + 1;
  268. titleBarsLength = 3;
  269. sideLineLength++;
  270. }
  271. }
  272. if (Driver is { }
  273. && Parent is { }
  274. && canDrawBorder
  275. && Thickness.Top > 0
  276. && maxTitleWidth > 0
  277. && Settings.FastHasFlags (BorderSettings.Title)
  278. && !string.IsNullOrEmpty (Parent?.Title))
  279. {
  280. Rectangle titleRect = new (borderBounds.X + 2, titleY, maxTitleWidth, 1);
  281. Parent.TitleTextFormatter.Draw (driver: Driver, screen: titleRect, normalColor: GetAttributeForRole (Parent.HasFocus ? VisualRole.Focus : VisualRole.Normal), hotColor: 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 (driver: Driver, location: new (screenBounds.X, screenBounds.Y));
  436. }
  437. // Redraw title
  438. if (drawTop && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title))
  439. {
  440. 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));
  441. }
  442. //Left
  443. var vruler = new Ruler { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical };
  444. if (drawLeft)
  445. {
  446. vruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y + 1), start: 1);
  447. }
  448. // Bottom
  449. if (drawBottom)
  450. {
  451. hruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y + screenBounds.Height - 1));
  452. }
  453. // Right
  454. if (drawRight)
  455. {
  456. vruler.Draw (driver: Driver, location: new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), start: 1);
  457. }
  458. }
  459. // TODO: This should not be done on each draw?
  460. if (Settings.FastHasFlags (BorderSettings.Gradient))
  461. {
  462. SetupGradientLineCanvas (lc!, screenBounds);
  463. }
  464. else
  465. {
  466. lc!.Fill = null;
  467. }
  468. }
  469. return true;
  470. }
  471. /// <summary>
  472. /// Gets the subview used to render <see cref="ViewDiagnosticFlags.DrawIndicator"/>.
  473. /// </summary>
  474. public SpinnerView? DrawIndicator { get; private set; }
  475. private void SetupGradientLineCanvas (LineCanvas lc, Rectangle rect)
  476. {
  477. GetAppealingGradientColors (out List<Color> stops, out List<int> steps);
  478. var g = new Gradient (stops, steps);
  479. var fore = new GradientFill (rect, g, GradientDirection.Diagonal);
  480. var back = new SolidFill (GetAttributeForRole (VisualRole.Normal).Background);
  481. lc.Fill = new (fore, back);
  482. }
  483. private static void GetAppealingGradientColors (out List<Color> stops, out List<int> steps)
  484. {
  485. // Define the colors of the gradient stops with more appealing colors
  486. stops =
  487. [
  488. new (0, 128, 255), // Bright Blue
  489. new (0, 255, 128), // Bright Green
  490. new (255, 255), // Bright Yellow
  491. new (255, 128), // Bright Orange
  492. new (255, 0, 128)
  493. ];
  494. // Define the number of steps between each color for smoother transitions
  495. // If we pass only a single value then it will assume equal steps between all pairs
  496. steps = [15];
  497. }
  498. }