LineCanvas.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. /// <summary>Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines.</summary>
  4. public class LineCanvas : IDisposable
  5. {
  6. /// <summary>
  7. /// Optional <see cref="FillPair"/> which when present overrides the <see cref="StraightLine.Attribute"/>
  8. /// (colors) of lines in the canvas. This can be used e.g. to apply a global <see cref="GradientFill"/>
  9. /// across all lines.
  10. /// </summary>
  11. public FillPair? Fill { get; set; }
  12. private readonly List<StraightLine> _lines = [];
  13. private readonly Dictionary<IntersectionRuneType, IntersectionRuneResolver> _runeResolvers = new ()
  14. {
  15. {
  16. IntersectionRuneType.ULCorner,
  17. new ULIntersectionRuneResolver ()
  18. },
  19. {
  20. IntersectionRuneType.URCorner,
  21. new URIntersectionRuneResolver ()
  22. },
  23. {
  24. IntersectionRuneType.LLCorner,
  25. new LLIntersectionRuneResolver ()
  26. },
  27. {
  28. IntersectionRuneType.LRCorner,
  29. new LRIntersectionRuneResolver ()
  30. },
  31. {
  32. IntersectionRuneType.TopTee,
  33. new TopTeeIntersectionRuneResolver ()
  34. },
  35. {
  36. IntersectionRuneType.LeftTee,
  37. new LeftTeeIntersectionRuneResolver ()
  38. },
  39. {
  40. IntersectionRuneType.RightTee,
  41. new RightTeeIntersectionRuneResolver ()
  42. },
  43. {
  44. IntersectionRuneType.BottomTee,
  45. new BottomTeeIntersectionRuneResolver ()
  46. },
  47. {
  48. IntersectionRuneType.Cross,
  49. new CrossIntersectionRuneResolver ()
  50. }
  51. // TODO: Add other resolvers
  52. };
  53. private Rectangle _cachedViewport;
  54. /// <summary>Creates a new instance.</summary>
  55. public LineCanvas ()
  56. {
  57. // TODO: Refactor ConfigurationManager to not use an event handler for this.
  58. // Instead, have it call a method on any class appropriately attributed
  59. // to update the cached values. See Issue #2871
  60. Applied += ConfigurationManager_Applied;
  61. }
  62. /// <summary>Creates a new instance with the given <paramref name="lines"/>.</summary>
  63. /// <param name="lines">Initial lines for the canvas.</param>
  64. public LineCanvas (IEnumerable<StraightLine> lines) : this () { _lines = lines.ToList (); }
  65. /// <summary>
  66. /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the line that is
  67. /// furthest left/top and Size is defined by the line that extends the furthest right/bottom.
  68. /// </summary>
  69. public Rectangle Viewport
  70. {
  71. get
  72. {
  73. if (_cachedViewport.IsEmpty)
  74. {
  75. if (_lines.Count == 0)
  76. {
  77. return _cachedViewport;
  78. }
  79. Rectangle viewport = _lines [0].Viewport;
  80. for (var i = 1; i < _lines.Count; i++)
  81. {
  82. viewport = Rectangle.Union (viewport, _lines [i].Viewport);
  83. }
  84. if (viewport is { Width: 0 } or { Height: 0 })
  85. {
  86. viewport = viewport with
  87. {
  88. Width = Math.Clamp (viewport.Width, 1, short.MaxValue),
  89. Height = Math.Clamp (viewport.Height, 1, short.MaxValue)
  90. };
  91. }
  92. _cachedViewport = viewport;
  93. }
  94. return _cachedViewport;
  95. }
  96. }
  97. /// <summary>Gets the lines in the canvas.</summary>
  98. public IReadOnlyCollection<StraightLine> Lines => _lines.AsReadOnly ();
  99. /// <inheritdoc/>
  100. public void Dispose () { Applied -= ConfigurationManager_Applied; }
  101. /// <summary>
  102. /// <para>Adds a new <paramref name="length"/> long line to the canvas starting at <paramref name="start"/>.</para>
  103. /// <para>
  104. /// Use positive <paramref name="length"/> for the line to extend Right and negative for Left when
  105. /// <see cref="Orientation"/> is <see cref="Orientation.Horizontal"/>.
  106. /// </para>
  107. /// <para>
  108. /// Use positive <paramref name="length"/> for the line to extend Down and negative for Up when
  109. /// <see cref="Orientation"/> is <see cref="Orientation.Vertical"/>.
  110. /// </para>
  111. /// </summary>
  112. /// <param name="start">Starting point.</param>
  113. /// <param name="length">
  114. /// The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for
  115. /// Up/Left.
  116. /// </param>
  117. /// <param name="orientation">The direction of the line.</param>
  118. /// <param name="style">The style of line to use</param>
  119. /// <param name="attribute"></param>
  120. public void AddLine (
  121. Point start,
  122. int length,
  123. Orientation orientation,
  124. LineStyle style,
  125. Attribute? attribute = null
  126. )
  127. {
  128. _cachedViewport = Rectangle.Empty;
  129. _lines.Add (new (start, length, orientation, style, attribute));
  130. }
  131. /// <summary>Adds a new line to the canvas</summary>
  132. /// <param name="line"></param>
  133. public void AddLine (StraightLine line)
  134. {
  135. _cachedViewport = Rectangle.Empty;
  136. _lines.Add (line);
  137. }
  138. /// <summary>Clears all lines from the LineCanvas.</summary>
  139. public void Clear ()
  140. {
  141. _cachedViewport = Rectangle.Empty;
  142. _lines.Clear ();
  143. _exclusionRegion = null;
  144. }
  145. private Region? _exclusionRegion;
  146. /// <summary>
  147. /// Gets the list of locations that will be excluded from <see cref="GetCellMap"/> and <see cref="GetMap()"/>.
  148. /// </summary>
  149. public void Exclude (Region region)
  150. {
  151. _exclusionRegion ??= new Region ();
  152. _exclusionRegion.Union (region);
  153. }
  154. /// <summary>
  155. /// Clears any cached states from the canvas Call this method if you make changes to lines that have already been
  156. /// added.
  157. /// </summary>
  158. public void ClearCache () { _cachedViewport = Rectangle.Empty; }
  159. /// <summary>
  160. /// Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
  161. /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
  162. /// intersection symbols.
  163. /// </summary>
  164. /// <returns>A map of all the points within the canvas.</returns>
  165. public Dictionary<Point, Cell?> GetCellMap ()
  166. {
  167. Dictionary<Point, Cell?> map = new ();
  168. // walk through each pixel of the bitmap
  169. for (int y = Viewport.Y; y < Viewport.Y + Viewport.Height; y++)
  170. {
  171. for (int x = Viewport.X; x < Viewport.X + Viewport.Width; x++)
  172. {
  173. IntersectionDefinition? [] intersects = _lines
  174. .Select (l => l.Intersects (x, y))
  175. .Where (i => i is { })
  176. .ToArray ();
  177. Cell? cell = GetCellForIntersects (Application.Driver, intersects);
  178. if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false)
  179. {
  180. map.Add (new (x, y), cell);
  181. }
  182. }
  183. }
  184. return map;
  185. }
  186. // TODO: Unless there's an obvious use case for this API we should delete it in favor of the
  187. // simpler version that doesn't take an area.
  188. /// <summary>
  189. /// Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
  190. /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
  191. /// intersection symbols.
  192. /// </summary>
  193. /// <param name="inArea">A rectangle to constrain the search by.</param>
  194. /// <returns>A map of the points within the canvas that intersect with <paramref name="inArea"/>.</returns>
  195. public Dictionary<Point, Rune> GetMap (Rectangle inArea)
  196. {
  197. Dictionary<Point, Rune> map = new ();
  198. // walk through each pixel of the bitmap
  199. for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++)
  200. {
  201. for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
  202. {
  203. IntersectionDefinition? [] intersects = _lines
  204. .Select (l => l.Intersects (x, y))
  205. .Where (i => i is { })
  206. .ToArray ();
  207. Rune? rune = GetRuneForIntersects (Application.Driver, intersects);
  208. if (rune is { } && _exclusionRegion?.Contains (x, y) is null or false)
  209. {
  210. map.Add (new (x, y), rune.Value);
  211. }
  212. }
  213. }
  214. return map;
  215. }
  216. /// <summary>
  217. /// Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
  218. /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
  219. /// intersection symbols.
  220. /// </summary>
  221. /// <returns>A map of all the points within the canvas.</returns>
  222. public Dictionary<Point, Rune> GetMap () { return GetMap (Viewport); }
  223. /// <summary>Merges one line canvas into this one.</summary>
  224. /// <param name="lineCanvas"></param>
  225. public void Merge (LineCanvas lineCanvas)
  226. {
  227. foreach (StraightLine line in lineCanvas._lines)
  228. {
  229. AddLine (line);
  230. }
  231. if (lineCanvas._exclusionRegion is { })
  232. {
  233. _exclusionRegion ??= new ();
  234. _exclusionRegion.Union (lineCanvas._exclusionRegion);
  235. }
  236. }
  237. /// <summary>Removes the last line added to the canvas</summary>
  238. /// <returns></returns>
  239. public StraightLine RemoveLastLine ()
  240. {
  241. StraightLine? l = _lines.LastOrDefault ();
  242. if (l is { })
  243. {
  244. _lines.Remove (l);
  245. }
  246. return l!;
  247. }
  248. /// <summary>
  249. /// Returns the contents of the line canvas rendered to a string. The string will include all columns and rows,
  250. /// even if <see cref="Viewport"/> has negative coordinates. For example, if the canvas contains a single line that
  251. /// starts at (-1,-1) with a length of 2, the rendered string will have a length of 2.
  252. /// </summary>
  253. /// <returns>The canvas rendered to a string.</returns>
  254. public override string ToString ()
  255. {
  256. if (Viewport.IsEmpty)
  257. {
  258. return string.Empty;
  259. }
  260. // Generate the rune map for the entire canvas
  261. Dictionary<Point, Rune> runeMap = GetMap ();
  262. // Create the rune canvas
  263. Rune [,] canvas = new Rune [Viewport.Height, Viewport.Width];
  264. // Copy the rune map to the canvas, adjusting for any negative coordinates
  265. foreach (KeyValuePair<Point, Rune> kvp in runeMap)
  266. {
  267. int x = kvp.Key.X - Viewport.X;
  268. int y = kvp.Key.Y - Viewport.Y;
  269. canvas [y, x] = kvp.Value;
  270. }
  271. // Convert the canvas to a string
  272. var sb = new StringBuilder ();
  273. for (var y = 0; y < canvas.GetLength (0); y++)
  274. {
  275. for (var x = 0; x < canvas.GetLength (1); x++)
  276. {
  277. Rune r = canvas [y, x];
  278. sb.Append (r.Value == 0 ? ' ' : r.ToString ());
  279. }
  280. if (y < canvas.GetLength (0) - 1)
  281. {
  282. sb.AppendLine ();
  283. }
  284. }
  285. return sb.ToString ();
  286. }
  287. private bool All (IntersectionDefinition? [] intersects, Orientation orientation) { return intersects.All (i => i!.Line.Orientation == orientation); }
  288. private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
  289. {
  290. foreach (KeyValuePair<IntersectionRuneType, IntersectionRuneResolver> irr in _runeResolvers)
  291. {
  292. irr.Value.SetGlyphs ();
  293. }
  294. }
  295. /// <summary>
  296. /// Returns true if all requested <paramref name="types"/> appear in <paramref name="intersects"/> and there are
  297. /// no additional <see cref="IntersectionRuneType"/>
  298. /// </summary>
  299. /// <param name="intersects"></param>
  300. /// <param name="types"></param>
  301. /// <returns></returns>
  302. private bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return intersects.SetEquals (types); }
  303. private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects)
  304. {
  305. return Fill != null ? Fill.GetAttribute (intersects [0]!.Point) : intersects [0]!.Line.Attribute;
  306. }
  307. private Cell? GetCellForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects)
  308. {
  309. if (!intersects.Any ())
  310. {
  311. return null;
  312. }
  313. var cell = new Cell ();
  314. Rune? rune = GetRuneForIntersects (driver, intersects);
  315. if (rune.HasValue)
  316. {
  317. cell.Rune = rune.Value;
  318. }
  319. cell.Attribute = GetAttributeForIntersects (intersects);
  320. return cell;
  321. }
  322. private Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects)
  323. {
  324. if (!intersects.Any ())
  325. {
  326. return null;
  327. }
  328. IntersectionRuneType runeType = GetRuneTypeForIntersects (intersects);
  329. if (_runeResolvers.TryGetValue (runeType, out IntersectionRuneResolver? resolver))
  330. {
  331. return resolver.GetRuneForIntersects (driver, intersects);
  332. }
  333. // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
  334. bool useDouble = intersects.Any (i => i?.Line.Style == LineStyle.Double);
  335. bool useDashed = intersects.Any (
  336. i => i?.Line.Style == LineStyle.Dashed
  337. || i?.Line.Style == LineStyle.RoundedDashed
  338. );
  339. bool useDotted = intersects.Any (
  340. i => i?.Line.Style == LineStyle.Dotted
  341. || i?.Line.Style == LineStyle.RoundedDotted
  342. );
  343. // horiz and vert lines same as Single for Rounded
  344. bool useThick = intersects.Any (i => i?.Line.Style == LineStyle.Heavy);
  345. bool useThickDashed = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDashed);
  346. bool useThickDotted = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDotted);
  347. // TODO: Support ruler
  348. //var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
  349. // TODO: maybe make these resolvers too for simplicity?
  350. switch (runeType)
  351. {
  352. case IntersectionRuneType.None:
  353. return null;
  354. case IntersectionRuneType.Dot:
  355. return Glyphs.Dot;
  356. case IntersectionRuneType.HLine:
  357. if (useDouble)
  358. {
  359. return Glyphs.HLineDbl;
  360. }
  361. if (useDashed)
  362. {
  363. return Glyphs.HLineDa2;
  364. }
  365. if (useDotted)
  366. {
  367. return Glyphs.HLineDa3;
  368. }
  369. return useThick ? Glyphs.HLineHv :
  370. useThickDashed ? Glyphs.HLineHvDa2 :
  371. useThickDotted ? Glyphs.HLineHvDa3 : Glyphs.HLine;
  372. case IntersectionRuneType.VLine:
  373. if (useDouble)
  374. {
  375. return Glyphs.VLineDbl;
  376. }
  377. if (useDashed)
  378. {
  379. return Glyphs.VLineDa3;
  380. }
  381. if (useDotted)
  382. {
  383. return Glyphs.VLineDa4;
  384. }
  385. return useThick ? Glyphs.VLineHv :
  386. useThickDashed ? Glyphs.VLineHvDa3 :
  387. useThickDotted ? Glyphs.VLineHvDa4 : Glyphs.VLine;
  388. default:
  389. throw new (
  390. "Could not find resolver or switch case for "
  391. + nameof (runeType)
  392. + ":"
  393. + runeType
  394. );
  395. }
  396. }
  397. private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition? [] intersects)
  398. {
  399. HashSet<IntersectionType> set = new (intersects.Select (i => i!.Type));
  400. #region Cross Conditions
  401. if (Has (
  402. set,
  403. IntersectionType.PassOverHorizontal,
  404. IntersectionType.PassOverVertical
  405. ))
  406. {
  407. return IntersectionRuneType.Cross;
  408. }
  409. if (Has (
  410. set,
  411. IntersectionType.PassOverVertical,
  412. IntersectionType.StartLeft,
  413. IntersectionType.StartRight
  414. ))
  415. {
  416. return IntersectionRuneType.Cross;
  417. }
  418. if (Has (
  419. set,
  420. IntersectionType.PassOverHorizontal,
  421. IntersectionType.StartUp,
  422. IntersectionType.StartDown
  423. ))
  424. {
  425. return IntersectionRuneType.Cross;
  426. }
  427. if (Has (
  428. set,
  429. IntersectionType.StartLeft,
  430. IntersectionType.StartRight,
  431. IntersectionType.StartUp,
  432. IntersectionType.StartDown
  433. ))
  434. {
  435. return IntersectionRuneType.Cross;
  436. }
  437. #endregion
  438. #region Corner Conditions
  439. if (Exactly (
  440. set,
  441. IntersectionType.StartRight,
  442. IntersectionType.StartDown
  443. ))
  444. {
  445. return IntersectionRuneType.ULCorner;
  446. }
  447. if (Exactly (
  448. set,
  449. IntersectionType.StartLeft,
  450. IntersectionType.StartDown
  451. ))
  452. {
  453. return IntersectionRuneType.URCorner;
  454. }
  455. if (Exactly (
  456. set,
  457. IntersectionType.StartUp,
  458. IntersectionType.StartLeft
  459. ))
  460. {
  461. return IntersectionRuneType.LRCorner;
  462. }
  463. if (Exactly (
  464. set,
  465. IntersectionType.StartUp,
  466. IntersectionType.StartRight
  467. ))
  468. {
  469. return IntersectionRuneType.LLCorner;
  470. }
  471. #endregion Corner Conditions
  472. #region T Conditions
  473. if (Has (
  474. set,
  475. IntersectionType.PassOverHorizontal,
  476. IntersectionType.StartDown
  477. ))
  478. {
  479. return IntersectionRuneType.TopTee;
  480. }
  481. if (Has (
  482. set,
  483. IntersectionType.StartRight,
  484. IntersectionType.StartLeft,
  485. IntersectionType.StartDown
  486. ))
  487. {
  488. return IntersectionRuneType.TopTee;
  489. }
  490. if (Has (
  491. set,
  492. IntersectionType.PassOverHorizontal,
  493. IntersectionType.StartUp
  494. ))
  495. {
  496. return IntersectionRuneType.BottomTee;
  497. }
  498. if (Has (
  499. set,
  500. IntersectionType.StartRight,
  501. IntersectionType.StartLeft,
  502. IntersectionType.StartUp
  503. ))
  504. {
  505. return IntersectionRuneType.BottomTee;
  506. }
  507. if (Has (
  508. set,
  509. IntersectionType.PassOverVertical,
  510. IntersectionType.StartRight
  511. ))
  512. {
  513. return IntersectionRuneType.LeftTee;
  514. }
  515. if (Has (
  516. set,
  517. IntersectionType.StartRight,
  518. IntersectionType.StartDown,
  519. IntersectionType.StartUp
  520. ))
  521. {
  522. return IntersectionRuneType.LeftTee;
  523. }
  524. if (Has (
  525. set,
  526. IntersectionType.PassOverVertical,
  527. IntersectionType.StartLeft
  528. ))
  529. {
  530. return IntersectionRuneType.RightTee;
  531. }
  532. if (Has (
  533. set,
  534. IntersectionType.StartLeft,
  535. IntersectionType.StartDown,
  536. IntersectionType.StartUp
  537. ))
  538. {
  539. return IntersectionRuneType.RightTee;
  540. }
  541. #endregion
  542. if (All (intersects, Orientation.Horizontal))
  543. {
  544. return IntersectionRuneType.HLine;
  545. }
  546. if (All (intersects, Orientation.Vertical))
  547. {
  548. return IntersectionRuneType.VLine;
  549. }
  550. return IntersectionRuneType.Dot;
  551. }
  552. /// <summary>
  553. /// Returns true if the <paramref name="intersects"/> collection has all the <paramref name="types"/> specified
  554. /// (i.e. AND).
  555. /// </summary>
  556. /// <param name="intersects"></param>
  557. /// <param name="types"></param>
  558. /// <returns></returns>
  559. private bool Has (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return types.All (t => intersects.Contains (t)); }
  560. private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver
  561. {
  562. public override void SetGlyphs ()
  563. {
  564. _round = Glyphs.BottomTee;
  565. _doubleH = Glyphs.BottomTeeDblH;
  566. _doubleV = Glyphs.BottomTeeDblV;
  567. _doubleBoth = Glyphs.BottomTeeDbl;
  568. _thickH = Glyphs.BottomTeeHvH;
  569. _thickV = Glyphs.BottomTeeHvV;
  570. _thickBoth = Glyphs.BottomTeeHvDblH;
  571. _normal = Glyphs.BottomTee;
  572. }
  573. }
  574. private class CrossIntersectionRuneResolver : IntersectionRuneResolver
  575. {
  576. public override void SetGlyphs ()
  577. {
  578. _round = Glyphs.Cross;
  579. _doubleH = Glyphs.CrossDblH;
  580. _doubleV = Glyphs.CrossDblV;
  581. _doubleBoth = Glyphs.CrossDbl;
  582. _thickH = Glyphs.CrossHvH;
  583. _thickV = Glyphs.CrossHvV;
  584. _thickBoth = Glyphs.CrossHv;
  585. _normal = Glyphs.Cross;
  586. }
  587. }
  588. private abstract class IntersectionRuneResolver
  589. {
  590. internal Rune _doubleBoth;
  591. internal Rune _doubleH;
  592. internal Rune _doubleV;
  593. internal Rune _normal;
  594. internal Rune _round;
  595. internal Rune _thickBoth;
  596. internal Rune _thickH;
  597. internal Rune _thickV;
  598. public IntersectionRuneResolver () { SetGlyphs (); }
  599. public Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects)
  600. {
  601. bool useRounded = intersects.Any (
  602. i => i?.Line.Length != 0
  603. && (
  604. i?.Line.Style == LineStyle.Rounded
  605. || i?.Line.Style
  606. == LineStyle.RoundedDashed
  607. || i?.Line.Style
  608. == LineStyle.RoundedDotted)
  609. );
  610. // Note that there aren't any glyphs for intersections of double lines with heavy lines
  611. bool doubleHorizontal = intersects.Any (
  612. l => l?.Line.Orientation == Orientation.Horizontal
  613. && l.Line.Style == LineStyle.Double
  614. );
  615. bool doubleVertical = intersects.Any (
  616. l => l?.Line.Orientation == Orientation.Vertical
  617. && l.Line.Style == LineStyle.Double
  618. );
  619. bool thickHorizontal = intersects.Any (
  620. l => l?.Line.Orientation == Orientation.Horizontal
  621. && (
  622. l.Line.Style == LineStyle.Heavy
  623. || l.Line.Style == LineStyle.HeavyDashed
  624. || l.Line.Style == LineStyle.HeavyDotted)
  625. );
  626. bool thickVertical = intersects.Any (
  627. l => l?.Line.Orientation == Orientation.Vertical
  628. && (
  629. l.Line.Style == LineStyle.Heavy
  630. || l.Line.Style == LineStyle.HeavyDashed
  631. || l.Line.Style == LineStyle.HeavyDotted)
  632. );
  633. if (doubleHorizontal)
  634. {
  635. return doubleVertical ? _doubleBoth : _doubleH;
  636. }
  637. if (doubleVertical)
  638. {
  639. return _doubleV;
  640. }
  641. if (thickHorizontal)
  642. {
  643. return thickVertical ? _thickBoth : _thickH;
  644. }
  645. if (thickVertical)
  646. {
  647. return _thickV;
  648. }
  649. return useRounded ? _round : _normal;
  650. }
  651. /// <summary>
  652. /// Sets the glyphs used. Call this method after construction and any time ConfigurationManager has updated the
  653. /// settings.
  654. /// </summary>
  655. public abstract void SetGlyphs ();
  656. }
  657. private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver
  658. {
  659. public override void SetGlyphs ()
  660. {
  661. _round = Glyphs.LeftTee;
  662. _doubleH = Glyphs.LeftTeeDblH;
  663. _doubleV = Glyphs.LeftTeeDblV;
  664. _doubleBoth = Glyphs.LeftTeeDbl;
  665. _thickH = Glyphs.LeftTeeHvH;
  666. _thickV = Glyphs.LeftTeeHvV;
  667. _thickBoth = Glyphs.LeftTeeHvDblH;
  668. _normal = Glyphs.LeftTee;
  669. }
  670. }
  671. private class LLIntersectionRuneResolver : IntersectionRuneResolver
  672. {
  673. public override void SetGlyphs ()
  674. {
  675. _round = Glyphs.LLCornerR;
  676. _doubleH = Glyphs.LLCornerSingleDbl;
  677. _doubleV = Glyphs.LLCornerDblSingle;
  678. _doubleBoth = Glyphs.LLCornerDbl;
  679. _thickH = Glyphs.LLCornerLtHv;
  680. _thickV = Glyphs.LLCornerHvLt;
  681. _thickBoth = Glyphs.LLCornerHv;
  682. _normal = Glyphs.LLCorner;
  683. }
  684. }
  685. private class LRIntersectionRuneResolver : IntersectionRuneResolver
  686. {
  687. public override void SetGlyphs ()
  688. {
  689. _round = Glyphs.LRCornerR;
  690. _doubleH = Glyphs.LRCornerSingleDbl;
  691. _doubleV = Glyphs.LRCornerDblSingle;
  692. _doubleBoth = Glyphs.LRCornerDbl;
  693. _thickH = Glyphs.LRCornerLtHv;
  694. _thickV = Glyphs.LRCornerHvLt;
  695. _thickBoth = Glyphs.LRCornerHv;
  696. _normal = Glyphs.LRCorner;
  697. }
  698. }
  699. private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver
  700. {
  701. public override void SetGlyphs ()
  702. {
  703. _round = Glyphs.RightTee;
  704. _doubleH = Glyphs.RightTeeDblH;
  705. _doubleV = Glyphs.RightTeeDblV;
  706. _doubleBoth = Glyphs.RightTeeDbl;
  707. _thickH = Glyphs.RightTeeHvH;
  708. _thickV = Glyphs.RightTeeHvV;
  709. _thickBoth = Glyphs.RightTeeHvDblH;
  710. _normal = Glyphs.RightTee;
  711. }
  712. }
  713. private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver
  714. {
  715. public override void SetGlyphs ()
  716. {
  717. _round = Glyphs.TopTee;
  718. _doubleH = Glyphs.TopTeeDblH;
  719. _doubleV = Glyphs.TopTeeDblV;
  720. _doubleBoth = Glyphs.TopTeeDbl;
  721. _thickH = Glyphs.TopTeeHvH;
  722. _thickV = Glyphs.TopTeeHvV;
  723. _thickBoth = Glyphs.TopTeeHvDblH;
  724. _normal = Glyphs.TopTee;
  725. }
  726. }
  727. private class ULIntersectionRuneResolver : IntersectionRuneResolver
  728. {
  729. public override void SetGlyphs ()
  730. {
  731. _round = Glyphs.ULCornerR;
  732. _doubleH = Glyphs.ULCornerSingleDbl;
  733. _doubleV = Glyphs.ULCornerDblSingle;
  734. _doubleBoth = Glyphs.ULCornerDbl;
  735. _thickH = Glyphs.ULCornerLtHv;
  736. _thickV = Glyphs.ULCornerHvLt;
  737. _thickBoth = Glyphs.ULCornerHv;
  738. _normal = Glyphs.ULCorner;
  739. }
  740. }
  741. private class URIntersectionRuneResolver : IntersectionRuneResolver
  742. {
  743. public override void SetGlyphs ()
  744. {
  745. _round = Glyphs.URCornerR;
  746. _doubleH = Glyphs.URCornerSingleDbl;
  747. _doubleV = Glyphs.URCornerDblSingle;
  748. _doubleBoth = Glyphs.URCornerDbl;
  749. _thickH = Glyphs.URCornerHvLt;
  750. _thickV = Glyphs.URCornerLtHv;
  751. _thickBoth = Glyphs.URCornerHv;
  752. _normal = Glyphs.URCorner;
  753. }
  754. }
  755. }