ViewDrawingClippingTests.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  1. using System.Text;
  2. using UnitTests;
  3. using Xunit.Abstractions;
  4. namespace ViewBaseTests.Drawing;
  5. public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBase
  6. {
  7. #region GetClip / SetClip Tests
  8. [Fact]
  9. public void GetClip_ReturnsDriverClip ()
  10. {
  11. IDriver driver = CreateFakeDriver ();
  12. var region = new Region (new (10, 10, 20, 20));
  13. driver.Clip = region;
  14. View view = new () { Driver = driver };
  15. Region? result = view.GetClip ();
  16. Assert.NotNull (result);
  17. Assert.Equal (region, result);
  18. }
  19. [Fact]
  20. public void SetClip_NullRegion_DoesNothing ()
  21. {
  22. IDriver driver = CreateFakeDriver ();
  23. var original = new Region (new (5, 5, 10, 10));
  24. driver.Clip = original;
  25. View view = new () { Driver = driver };
  26. view.SetClip (null);
  27. Assert.Equal (original, driver.Clip);
  28. }
  29. [Fact]
  30. public void SetClip_ValidRegion_SetsDriverClip ()
  31. {
  32. IDriver driver = CreateFakeDriver ();
  33. var region = new Region (new (10, 10, 30, 30));
  34. View view = new () { Driver = driver };
  35. view.SetClip (region);
  36. Assert.Equal (region, driver.Clip);
  37. }
  38. #endregion
  39. #region SetClipToScreen Tests
  40. [Fact]
  41. public void SetClipToScreen_ReturnsPreviousClip ()
  42. {
  43. IDriver driver = CreateFakeDriver ();
  44. var original = new Region (new (5, 5, 10, 10));
  45. driver.Clip = original;
  46. View view = new () { Driver = driver };
  47. Region? previous = view.SetClipToScreen ();
  48. Assert.Equal (original, previous);
  49. Assert.NotEqual (original, driver.Clip);
  50. }
  51. [Fact]
  52. public void SetClipToScreen_SetsClipToScreen ()
  53. {
  54. IDriver driver = CreateFakeDriver ();
  55. View view = new () { Driver = driver };
  56. view.SetClipToScreen ();
  57. Assert.NotNull (driver.Clip);
  58. Assert.Equal (driver.Screen, driver.Clip.GetBounds ());
  59. }
  60. #endregion
  61. #region ExcludeFromClip Tests
  62. [Fact]
  63. public void ExcludeFromClip_Rectangle_NullDriver_DoesNotThrow ()
  64. {
  65. View view = new () { Driver = null };
  66. Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (5, 5, 10, 10)));
  67. Assert.Null (exception);
  68. }
  69. [Fact]
  70. public void ExcludeFromClip_Rectangle_ExcludesArea ()
  71. {
  72. IDriver driver = CreateFakeDriver ();
  73. driver.Clip = new (new (0, 0, 80, 25));
  74. View view = new () { Driver = driver };
  75. var toExclude = new Rectangle (10, 10, 20, 20);
  76. view.ExcludeFromClip (toExclude);
  77. // Verify the region was excluded
  78. Assert.NotNull (driver.Clip);
  79. Assert.False (driver.Clip.Contains (15, 15));
  80. }
  81. [Fact]
  82. public void ExcludeFromClip_Region_NullDriver_DoesNotThrow ()
  83. {
  84. View view = new () { Driver = null };
  85. Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Region (new (5, 5, 10, 10))));
  86. Assert.Null (exception);
  87. }
  88. [Fact]
  89. public void ExcludeFromClip_Region_ExcludesArea ()
  90. {
  91. IDriver driver = CreateFakeDriver ();
  92. driver.Clip = new (new (0, 0, 80, 25));
  93. View view = new () { Driver = driver };
  94. var toExclude = new Region (new (10, 10, 20, 20));
  95. view.ExcludeFromClip (toExclude);
  96. // Verify the region was excluded
  97. Assert.NotNull (driver.Clip);
  98. Assert.False (driver.Clip.Contains (15, 15));
  99. }
  100. #endregion
  101. #region AddFrameToClip Tests
  102. [Fact]
  103. public void AddFrameToClip_NullDriver_ReturnsNull ()
  104. {
  105. var view = new View { X = 0, Y = 0, Width = 10, Height = 10 };
  106. view.BeginInit ();
  107. view.EndInit ();
  108. Region? result = view.AddFrameToClip ();
  109. Assert.Null (result);
  110. }
  111. [Fact]
  112. public void AddFrameToClip_IntersectsWithFrame ()
  113. {
  114. IDriver driver = CreateFakeDriver ();
  115. driver.Clip = new (driver.Screen);
  116. var view = new View
  117. {
  118. X = 1,
  119. Y = 1,
  120. Width = 20,
  121. Height = 20,
  122. Driver = driver
  123. };
  124. view.BeginInit ();
  125. view.EndInit ();
  126. view.LayoutSubViews ();
  127. Region? previous = view.AddFrameToClip ();
  128. Assert.NotNull (previous);
  129. Assert.NotNull (driver.Clip);
  130. // The clip should now be the intersection of the screen and the view's frame
  131. var expectedBounds = new Rectangle (1, 1, 20, 20);
  132. Assert.Equal (expectedBounds, driver.Clip.GetBounds ());
  133. }
  134. #endregion
  135. #region AddViewportToClip Tests
  136. [Fact]
  137. public void AddViewportToClip_NullDriver_ReturnsNull ()
  138. {
  139. var view = new View { X = 0, Y = 0, Width = 10, Height = 10 };
  140. view.BeginInit ();
  141. view.EndInit ();
  142. Region? result = view.AddViewportToClip ();
  143. Assert.Null (result);
  144. }
  145. [Fact]
  146. public void AddViewportToClip_IntersectsWithViewport ()
  147. {
  148. IDriver driver = CreateFakeDriver ();
  149. driver.Clip = new (driver.Screen);
  150. var view = new View
  151. {
  152. X = 1,
  153. Y = 1,
  154. Width = 20,
  155. Height = 20,
  156. Driver = driver
  157. };
  158. view.BeginInit ();
  159. view.EndInit ();
  160. view.LayoutSubViews ();
  161. Region? previous = view.AddViewportToClip ();
  162. Assert.NotNull (previous);
  163. Assert.NotNull (driver.Clip);
  164. // The clip should be the viewport area
  165. Rectangle viewportScreen = view.ViewportToScreen (new Rectangle (Point.Empty, view.Viewport.Size));
  166. Assert.Equal (viewportScreen, driver.Clip.GetBounds ());
  167. }
  168. [Fact]
  169. public void AddViewportToClip_WithClipContentOnly_LimitsToVisibleContent ()
  170. {
  171. IDriver driver = CreateFakeDriver ();
  172. driver.Clip = new (driver.Screen);
  173. var view = new View
  174. {
  175. X = 1,
  176. Y = 1,
  177. Width = 20,
  178. Height = 20,
  179. Driver = driver
  180. };
  181. view.SetContentSize (new Size (100, 100));
  182. view.ViewportSettings = ViewportSettingsFlags.ClipContentOnly;
  183. view.BeginInit ();
  184. view.EndInit ();
  185. view.LayoutSubViews ();
  186. Region? previous = view.AddViewportToClip ();
  187. Assert.NotNull (previous);
  188. Assert.NotNull (driver.Clip);
  189. // The clip should be limited to visible content
  190. Rectangle visibleContent = view.ViewportToScreen (new Rectangle (new (-view.Viewport.X, -view.Viewport.Y), view.GetContentSize ()));
  191. Rectangle viewport = view.ViewportToScreen (new Rectangle (Point.Empty, view.Viewport.Size));
  192. Rectangle expected = Rectangle.Intersect (viewport, visibleContent);
  193. Assert.Equal (expected, driver.Clip.GetBounds ());
  194. }
  195. #endregion
  196. #region Clip Interaction Tests
  197. [Fact]
  198. public void ClipRegions_StackCorrectly_WithNestedViews ()
  199. {
  200. IDriver driver = CreateFakeDriver (100, 100);
  201. driver.Clip = new (driver.Screen);
  202. var superView = new View
  203. {
  204. X = 1,
  205. Y = 1,
  206. Width = 50,
  207. Height = 50,
  208. Driver = driver
  209. };
  210. superView.BeginInit ();
  211. superView.EndInit ();
  212. var view = new View
  213. {
  214. X = 5,
  215. Y = 5,
  216. Width = 30,
  217. Height = 30
  218. };
  219. superView.Add (view);
  220. superView.LayoutSubViews ();
  221. // Set clip to superView's frame
  222. Region? superViewClip = superView.AddFrameToClip ();
  223. Rectangle superViewBounds = driver.Clip.GetBounds ();
  224. // Now set clip to view's frame
  225. Region? viewClip = view.AddFrameToClip ();
  226. Rectangle viewBounds = driver.Clip.GetBounds ();
  227. // Child clip should be within superView clip
  228. Assert.True (superViewBounds.Contains (viewBounds.Location));
  229. // Restore superView clip
  230. view.SetClip (superViewClip);
  231. // Assert.Equal (superViewBounds, driver.Clip.GetBounds ());
  232. }
  233. [Fact]
  234. public void ClipRegions_RespectPreviousClip ()
  235. {
  236. IDriver driver = CreateFakeDriver ();
  237. var initialClip = new Region (new (20, 20, 40, 40));
  238. driver.Clip = initialClip;
  239. var view = new View
  240. {
  241. X = 1,
  242. Y = 1,
  243. Width = 60,
  244. Height = 60,
  245. Driver = driver
  246. };
  247. view.BeginInit ();
  248. view.EndInit ();
  249. view.LayoutSubViews ();
  250. Region? previous = view.AddFrameToClip ();
  251. // The new clip should be the intersection of the initial clip and the view's frame
  252. Rectangle expected = Rectangle.Intersect (
  253. initialClip.GetBounds (),
  254. view.FrameToScreen ()
  255. );
  256. Assert.Equal (expected, driver.Clip.GetBounds ());
  257. // Restore should give us back the original
  258. view.SetClip (previous);
  259. Assert.Equal (initialClip.GetBounds (), driver.Clip.GetBounds ());
  260. }
  261. #endregion
  262. #region Edge Cases
  263. [Fact]
  264. public void AddFrameToClip_EmptyFrame_WorksCorrectly ()
  265. {
  266. IDriver driver = CreateFakeDriver ();
  267. driver.Clip = new (driver.Screen);
  268. var view = new View
  269. {
  270. X = 1,
  271. Y = 1,
  272. Width = 0,
  273. Height = 0,
  274. Driver = driver
  275. };
  276. view.BeginInit ();
  277. view.EndInit ();
  278. view.LayoutSubViews ();
  279. Region? previous = view.AddFrameToClip ();
  280. Assert.NotNull (previous);
  281. Assert.NotNull (driver.Clip);
  282. }
  283. [Fact]
  284. public void AddViewportToClip_EmptyViewport_WorksCorrectly ()
  285. {
  286. IDriver driver = CreateFakeDriver ();
  287. driver.Clip = new (driver.Screen);
  288. var view = new View
  289. {
  290. X = 1,
  291. Y = 1,
  292. Width = 1, // Minimal size to have adornments
  293. Height = 1,
  294. Driver = driver
  295. };
  296. view.Border!.Thickness = new (1);
  297. view.BeginInit ();
  298. view.EndInit ();
  299. view.LayoutSubViews ();
  300. // With border thickness of 1, the viewport should be empty
  301. Assert.True (view.Viewport.Size.Width == 0 || view.Viewport.Size.Height == 0);
  302. Region? previous = view.AddViewportToClip ();
  303. Assert.NotNull (previous);
  304. }
  305. [Fact]
  306. public void ClipRegions_OutOfBounds_HandledCorrectly ()
  307. {
  308. IDriver driver = CreateFakeDriver ();
  309. driver.Clip = new (driver.Screen);
  310. var view = new View
  311. {
  312. X = 100, // Outside screen bounds
  313. Y = 100,
  314. Width = 20,
  315. Height = 20,
  316. Driver = driver
  317. };
  318. view.BeginInit ();
  319. view.EndInit ();
  320. view.LayoutSubViews ();
  321. Region? previous = view.AddFrameToClip ();
  322. Assert.NotNull (previous);
  323. // The clip should be empty since the view is outside the screen
  324. Assert.True (driver.Clip.IsEmpty () || !driver.Clip.Contains (100, 100));
  325. }
  326. #endregion
  327. #region Drawing Tests
  328. [Fact]
  329. public void Clip_Set_BeforeDraw_ClipsDrawing ()
  330. {
  331. IDriver driver = CreateFakeDriver ();
  332. var clip = new Region (new (10, 10, 10, 10));
  333. driver.Clip = clip;
  334. var view = new View
  335. {
  336. X = 0,
  337. Y = 0,
  338. Width = 50,
  339. Height = 50,
  340. Driver = driver
  341. };
  342. view.BeginInit ();
  343. view.EndInit ();
  344. view.LayoutSubViews ();
  345. view.Draw ();
  346. // Verify clip was used
  347. Assert.NotNull (driver.Clip);
  348. }
  349. [Fact]
  350. public void Draw_UpdatesDriverClip ()
  351. {
  352. IDriver driver = CreateFakeDriver ();
  353. driver.Clip = new (driver.Screen);
  354. var view = new View
  355. {
  356. X = 1,
  357. Y = 1,
  358. Width = 20,
  359. Height = 20,
  360. Driver = driver
  361. };
  362. view.BeginInit ();
  363. view.EndInit ();
  364. view.LayoutSubViews ();
  365. view.Draw ();
  366. // Clip should be updated to exclude the drawn view
  367. Assert.NotNull (driver.Clip);
  368. // Assert.False (driver.Clip.Contains (15, 15)); // Point inside the view should be excluded
  369. }
  370. [Fact]
  371. public void Draw_WithSubViews_ClipsCorrectly ()
  372. {
  373. IDriver driver = CreateFakeDriver ();
  374. driver.Clip = new (driver.Screen);
  375. var superView = new View
  376. {
  377. X = 1,
  378. Y = 1,
  379. Width = 50,
  380. Height = 50,
  381. Driver = driver
  382. };
  383. var view = new View { X = 5, Y = 5, Width = 20, Height = 20 };
  384. superView.Add (view);
  385. superView.BeginInit ();
  386. superView.EndInit ();
  387. superView.LayoutSubViews ();
  388. superView.Draw ();
  389. // Both superView and view should be excluded from clip
  390. Assert.NotNull (driver.Clip);
  391. // Assert.False (driver.Clip.Contains (15, 15)); // Point in superView should be excluded
  392. }
  393. /// <summary>
  394. /// Tests that wide glyphs (🍎) are correctly clipped when overlapped by bordered subviews
  395. /// at different column alignments (even vs odd). Demonstrates:
  396. /// 1. Full clipping at even columns (X=0, X=2)
  397. /// 2. Partial clipping at odd columns (X=1) resulting in half-glyphs (�)
  398. /// 3. The recursive draw flow and clip exclusion mechanism
  399. ///
  400. /// For detailed draw flow documentation, see ViewDrawingClippingTests.DrawFlow.md
  401. /// </summary>
  402. [Fact]
  403. public void Draw_WithBorderSubView_DrawsCorrectly ()
  404. {
  405. IApplication app = Application.Create ();
  406. app.Init ("fake");
  407. IDriver driver = app!.Driver!;
  408. driver.SetScreenSize (30, 20);
  409. driver!.Clip = new (driver.Screen);
  410. var superView = new Runnable ()
  411. {
  412. X = 0,
  413. Y = 0,
  414. Width = Dim.Auto () + 4,
  415. Height = Dim.Auto () + 1,
  416. Driver = driver
  417. };
  418. Rune codepoint = Glyphs.Apple;
  419. superView.DrawingContent += (s, e) =>
  420. {
  421. var view = s as View;
  422. for (var r = 0; r < view!.Viewport.Height; r++)
  423. {
  424. for (var c = 0; c < view.Viewport.Width; c += 2)
  425. {
  426. if (codepoint != default (Rune))
  427. {
  428. view.AddRune (c, r, codepoint);
  429. }
  430. }
  431. }
  432. e.DrawContext?.AddDrawnRectangle (view.Viewport);
  433. e.Cancel = true;
  434. };
  435. var viewWithBorderAtX0 = new View
  436. {
  437. Text = "viewWithBorderAtX0",
  438. BorderStyle = LineStyle.Dashed,
  439. X = 0,
  440. Y = 1,
  441. Width = Dim.Auto (),
  442. Height = 3
  443. };
  444. var viewWithBorderAtX1 = new View
  445. {
  446. Text = "viewWithBorderAtX1",
  447. BorderStyle = LineStyle.Dashed,
  448. X = 1,
  449. Y = Pos.Bottom (viewWithBorderAtX0) + 1,
  450. Width = Dim.Auto (),
  451. Height = 3
  452. };
  453. var viewWithBorderAtX2 = new View
  454. {
  455. Text = "viewWithBorderAtX2",
  456. BorderStyle = LineStyle.Dashed,
  457. X = 2,
  458. Y = Pos.Bottom (viewWithBorderAtX1) + 1,
  459. Width = Dim.Auto (),
  460. Height = 3
  461. };
  462. superView.Add (viewWithBorderAtX0, viewWithBorderAtX1, viewWithBorderAtX2);
  463. app.Begin (superView);
  464. // Begin calls LayoutAndDraw, so no need to call it again here
  465. // app.LayoutAndDraw();
  466. DriverAssert.AssertDriverContentsAre (
  467. """
  468. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  469. ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎
  470. ┆viewWithBorderAtX0┆🍎🍎🍎
  471. └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎
  472. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  473. �┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎
  474. �┆viewWithBorderAtX1┆ 🍎🍎
  475. �└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎
  476. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  477. 🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎
  478. 🍎┆viewWithBorderAtX2┆🍎🍎
  479. 🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎
  480. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  481. """,
  482. output,
  483. driver);
  484. DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┆viewWithBorderAtX0┆🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�┆viewWithBorderAtX1┆ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┆viewWithBorderAtX2┆🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
  485. output, driver);
  486. DriverImpl? driverImpl = driver as DriverImpl;
  487. FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
  488. output.WriteLine ("Driver Output After Redraw:\n" + driver.GetOutput().GetLastOutput());
  489. // BUGBUG: Border.set_LineStyle does not call SetNeedsDraw
  490. viewWithBorderAtX1!.Border!.LineStyle = LineStyle.Single;
  491. viewWithBorderAtX1.Border!.SetNeedsDraw ();
  492. app.LayoutAndDraw ();
  493. DriverAssert.AssertDriverContentsAre (
  494. """
  495. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  496. ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎
  497. ┆viewWithBorderAtX0┆🍎🍎🍎
  498. └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎
  499. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  500. �┌──────────────────┐ 🍎🍎
  501. �│viewWithBorderAtX1│ 🍎🍎
  502. �└──────────────────┘ 🍎🍎
  503. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  504. 🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎
  505. 🍎┆viewWithBorderAtX2┆🍎🍎
  506. 🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎
  507. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  508. """,
  509. output,
  510. driver);
  511. }
  512. [Fact]
  513. public void Draw_WithBorderSubView_At_Col1_In_WideGlyph_DrawsCorrectly ()
  514. {
  515. IApplication app = Application.Create ();
  516. app.Init ("fake");
  517. IDriver driver = app!.Driver!;
  518. driver.SetScreenSize (6, 3); // Minimal: 6 cols wide (3 for content + 2 for border + 1), 3 rows high (1 for content + 2 for border)
  519. driver!.Clip = new (driver.Screen);
  520. var superView = new Runnable ()
  521. {
  522. X = 0,
  523. Y = 0,
  524. Width = Dim.Fill (),
  525. Height = Dim.Fill (),
  526. Driver = driver
  527. };
  528. Rune codepoint = Glyphs.Apple;
  529. superView.DrawingContent += (s, e) =>
  530. {
  531. View? view = s as View;
  532. view?.AddStr (0, 0, "🍎🍎🍎🍎");
  533. view?.AddStr (0, 1, "🍎🍎🍎🍎");
  534. view?.AddStr (0, 2, "🍎🍎🍎🍎");
  535. e.DrawContext?.AddDrawnRectangle (view!.Viewport);
  536. e.Cancel = true;
  537. };
  538. // Minimal border at X=1 (odd column), Width=3, Height=3 (includes border)
  539. var viewWithBorder = new View
  540. {
  541. Text = "X",
  542. BorderStyle = LineStyle.Single,
  543. X = 1,
  544. Y = 0,
  545. Width = 3,
  546. Height = 3
  547. };
  548. superView.Add (viewWithBorder);
  549. app.Begin (superView);
  550. DriverAssert.AssertDriverContentsAre (
  551. """
  552. �┌─┐🍎
  553. �│X│🍎
  554. �└─┘🍎
  555. """,
  556. output,
  557. driver);
  558. DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�┌─┐🍎�│X│🍎�└─┘🍎",
  559. output, driver);
  560. DriverImpl? driverImpl = driver as DriverImpl;
  561. FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
  562. output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
  563. }
  564. [Fact]
  565. public void Draw_WithBorderSubView_At_Col3_In_WideGlyph_DrawsCorrectly ()
  566. {
  567. IApplication app = Application.Create ();
  568. app.Init ("fake");
  569. IDriver driver = app!.Driver!;
  570. driver.SetScreenSize (6, 3); // Screen: 6 cols wide, 3 rows high; enough for 3x3 border subview at col 3 plus content on the left
  571. driver!.Clip = new (driver.Screen);
  572. var superView = new Runnable ()
  573. {
  574. X = 0,
  575. Y = 0,
  576. Width = Dim.Fill (),
  577. Height = Dim.Fill (),
  578. Driver = driver
  579. };
  580. Rune codepoint = Glyphs.Apple;
  581. superView.DrawingContent += (s, e) =>
  582. {
  583. View? view = s as View;
  584. view?.AddStr (0, 0, "🍎🍎🍎🍎");
  585. view?.AddStr (0, 1, "🍎🍎🍎🍎");
  586. view?.AddStr (0, 2, "🍎🍎🍎🍎");
  587. e.DrawContext?.AddDrawnRectangle (view!.Viewport);
  588. e.Cancel = true;
  589. };
  590. // Minimal border at X=3 (odd column), Width=3, Height=3 (includes border)
  591. var viewWithBorder = new View
  592. {
  593. Text = "X",
  594. BorderStyle = LineStyle.Single,
  595. X = 3,
  596. Y = 0,
  597. Width = 3,
  598. Height = 3
  599. };
  600. superView.Add (viewWithBorder);
  601. app.Begin (superView);
  602. DriverAssert.AssertDriverContentsAre (
  603. """
  604. 🍎�┌─┐
  605. 🍎�│X│
  606. 🍎�└─┘
  607. """,
  608. output,
  609. driver);
  610. DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎�┌─┐🍎�│X│🍎�└─┘",
  611. output, driver);
  612. DriverImpl? driverImpl = driver as DriverImpl;
  613. FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
  614. output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
  615. }
  616. [Fact]
  617. public void Draw_NonVisibleView_DoesNotUpdateClip ()
  618. {
  619. IDriver driver = CreateFakeDriver ();
  620. var originalClip = new Region (driver.Screen);
  621. driver.Clip = originalClip.Clone ();
  622. var view = new View
  623. {
  624. X = 10,
  625. Y = 10,
  626. Width = 20,
  627. Height = 20,
  628. Visible = false,
  629. Driver = driver
  630. };
  631. view.BeginInit ();
  632. view.EndInit ();
  633. view.Draw ();
  634. // Clip should not be modified for invisible views
  635. Assert.True (driver.Clip.Equals (originalClip));
  636. }
  637. [Fact]
  638. public void ExcludeFromClip_ExcludesRegion ()
  639. {
  640. IDriver driver = CreateFakeDriver ();
  641. driver.Clip = new (driver.Screen);
  642. var view = new View
  643. {
  644. X = 10,
  645. Y = 10,
  646. Width = 20,
  647. Height = 20,
  648. Driver = driver
  649. };
  650. view.BeginInit ();
  651. view.EndInit ();
  652. view.LayoutSubViews ();
  653. var excludeRect = new Rectangle (15, 15, 10, 10);
  654. view.ExcludeFromClip (excludeRect);
  655. Assert.NotNull (driver.Clip);
  656. Assert.False (driver.Clip.Contains (20, 20)); // Point inside excluded rect should not be in clip
  657. }
  658. [Fact]
  659. public void ExcludeFromClip_WithNullClip_DoesNotThrow ()
  660. {
  661. IDriver driver = CreateFakeDriver ();
  662. driver.Clip = null!;
  663. var view = new View
  664. {
  665. X = 10,
  666. Y = 10,
  667. Width = 20,
  668. Height = 20,
  669. Driver = driver
  670. };
  671. Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (15, 15, 10, 10)));
  672. Assert.Null (exception);
  673. }
  674. #endregion
  675. #region Misc Tests
  676. [Fact]
  677. public void SetClip_SetsDriverClip ()
  678. {
  679. IDriver driver = CreateFakeDriver ();
  680. var view = new View
  681. {
  682. X = 10,
  683. Y = 10,
  684. Width = 20,
  685. Height = 20,
  686. Driver = driver
  687. };
  688. var newClip = new Region (new (5, 5, 30, 30));
  689. view.SetClip (newClip);
  690. Assert.Equal (newClip, driver.Clip);
  691. }
  692. [Fact (Skip = "See BUGBUG in SetClip")]
  693. public void SetClip_WithNullClip_ClearsClip ()
  694. {
  695. IDriver driver = CreateFakeDriver ();
  696. driver.Clip = new (new (10, 10, 20, 20));
  697. var view = new View
  698. {
  699. X = 10,
  700. Y = 10,
  701. Width = 20,
  702. Height = 20,
  703. Driver = driver
  704. };
  705. view.SetClip (null);
  706. Assert.Null (driver.Clip);
  707. }
  708. [Fact]
  709. public void Draw_Excludes_View_From_Clip ()
  710. {
  711. IDriver driver = CreateFakeDriver ();
  712. var originalClip = new Region (driver.Screen);
  713. driver.Clip = originalClip.Clone ();
  714. var view = new View
  715. {
  716. X = 10,
  717. Y = 10,
  718. Width = 20,
  719. Height = 20,
  720. Driver = driver
  721. };
  722. view.BeginInit ();
  723. view.EndInit ();
  724. view.LayoutSubViews ();
  725. Region clipWithViewExcluded = originalClip.Clone ();
  726. clipWithViewExcluded.Exclude (view.Frame);
  727. view.Draw ();
  728. Assert.Equal (clipWithViewExcluded, driver.Clip);
  729. Assert.NotNull (driver.Clip);
  730. }
  731. [Fact]
  732. public void Draw_EmptyViewport_DoesNotCrash ()
  733. {
  734. IDriver driver = CreateFakeDriver ();
  735. driver.Clip = new (driver.Screen);
  736. var view = new View
  737. {
  738. X = 10,
  739. Y = 10,
  740. Width = 1,
  741. Height = 1,
  742. Driver = driver
  743. };
  744. view.Border!.Thickness = new (1);
  745. view.BeginInit ();
  746. view.EndInit ();
  747. view.LayoutSubViews ();
  748. // With border of 1, viewport should be empty (0x0 or negative)
  749. Exception? exception = Record.Exception (() => view.Draw ());
  750. Assert.Null (exception);
  751. }
  752. [Fact]
  753. public void Draw_VeryLargeView_HandlesClippingCorrectly ()
  754. {
  755. IDriver driver = CreateFakeDriver ();
  756. driver.Clip = new (driver.Screen);
  757. var view = new View
  758. {
  759. X = 0,
  760. Y = 0,
  761. Width = 1000,
  762. Height = 1000,
  763. Driver = driver
  764. };
  765. view.BeginInit ();
  766. view.EndInit ();
  767. view.LayoutSubViews ();
  768. Exception? exception = Record.Exception (() => view.Draw ());
  769. Assert.Null (exception);
  770. }
  771. [Fact]
  772. public void Draw_NegativeCoordinates_HandlesClippingCorrectly ()
  773. {
  774. IDriver driver = CreateFakeDriver ();
  775. driver.Clip = new (driver.Screen);
  776. var view = new View
  777. {
  778. X = -10,
  779. Y = -10,
  780. Width = 50,
  781. Height = 50,
  782. Driver = driver
  783. };
  784. view.BeginInit ();
  785. view.EndInit ();
  786. view.LayoutSubViews ();
  787. Exception? exception = Record.Exception (() => view.Draw ());
  788. Assert.Null (exception);
  789. }
  790. [Fact]
  791. public void Draw_OutOfScreenBounds_HandlesClippingCorrectly ()
  792. {
  793. IDriver driver = CreateFakeDriver ();
  794. driver.Clip = new (driver.Screen);
  795. var view = new View
  796. {
  797. X = 100,
  798. Y = 100,
  799. Width = 50,
  800. Height = 50,
  801. Driver = driver
  802. };
  803. view.BeginInit ();
  804. view.EndInit ();
  805. view.LayoutSubViews ();
  806. Exception? exception = Record.Exception (() => view.Draw ());
  807. Assert.Null (exception);
  808. }
  809. #endregion
  810. }