ViewDrawingClippingTests.cs 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  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. driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
  464. app.Begin (superView);
  465. // Begin calls LayoutAndDraw, so no need to call it again here
  466. // app.LayoutAndDraw();
  467. DriverAssert.AssertDriverContentsAre (
  468. """
  469. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  470. ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎
  471. ┆viewWithBorderAtX0┆🍎🍎🍎
  472. └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎
  473. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  474. ①┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎
  475. ①┆viewWithBorderAtX1┆ 🍎🍎
  476. ①└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎
  477. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  478. 🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎
  479. 🍎┆viewWithBorderAtX2┆🍎🍎
  480. 🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎
  481. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  482. """,
  483. output,
  484. driver);
  485. 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",
  486. output, driver);
  487. DriverImpl? driverImpl = driver as DriverImpl;
  488. FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
  489. output.WriteLine ("Driver Output After Redraw:\n" + driver.GetOutput().GetLastOutput());
  490. // BUGBUG: Border.set_LineStyle does not call SetNeedsDraw
  491. viewWithBorderAtX1!.Border!.LineStyle = LineStyle.Single;
  492. viewWithBorderAtX1.Border!.SetNeedsDraw ();
  493. app.LayoutAndDraw ();
  494. DriverAssert.AssertDriverContentsAre (
  495. """
  496. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  497. ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎
  498. ┆viewWithBorderAtX0┆🍎🍎🍎
  499. └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎
  500. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  501. ①┌──────────────────┐ 🍎🍎
  502. ①│viewWithBorderAtX1│ 🍎🍎
  503. ①└──────────────────┘ 🍎🍎
  504. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  505. 🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎
  506. 🍎┆viewWithBorderAtX2┆🍎🍎
  507. 🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎
  508. 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
  509. """,
  510. output,
  511. driver);
  512. }
  513. [Fact]
  514. public void Draw_WithBorderSubView_At_Col1_In_WideGlyph_DrawsCorrectly ()
  515. {
  516. IApplication app = Application.Create ();
  517. app.Init ("fake");
  518. IDriver driver = app!.Driver!;
  519. driver.SetScreenSize (6, 3); // Minimal: 6 cols wide (3 for content + 2 for border + 1), 3 rows high (1 for content + 2 for border)
  520. driver!.Clip = new (driver.Screen);
  521. var superView = new Runnable ()
  522. {
  523. X = 0,
  524. Y = 0,
  525. Width = Dim.Fill (),
  526. Height = Dim.Fill (),
  527. Driver = driver
  528. };
  529. Rune codepoint = Glyphs.Apple;
  530. superView.DrawingContent += (s, e) =>
  531. {
  532. View? view = s as View;
  533. view?.AddStr (0, 0, "🍎🍎🍎🍎");
  534. view?.AddStr (0, 1, "🍎🍎🍎🍎");
  535. view?.AddStr (0, 2, "🍎🍎🍎🍎");
  536. e.DrawContext?.AddDrawnRectangle (view!.Viewport);
  537. e.Cancel = true;
  538. };
  539. // Minimal border at X=1 (odd column), Width=3, Height=3 (includes border)
  540. var viewWithBorder = new View
  541. {
  542. Text = "X",
  543. BorderStyle = LineStyle.Single,
  544. X = 1,
  545. Y = 0,
  546. Width = 3,
  547. Height = 3
  548. };
  549. superView.Add (viewWithBorder);
  550. driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
  551. app.Begin (superView);
  552. DriverAssert.AssertDriverContentsAre (
  553. """
  554. ①┌─┐🍎
  555. ①│X│🍎
  556. ①└─┘🍎
  557. """,
  558. output,
  559. driver);
  560. DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①┌─┐🍎①│X│🍎①└─┘🍎",
  561. output, driver);
  562. DriverImpl? driverImpl = driver as DriverImpl;
  563. FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
  564. output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
  565. }
  566. [Fact]
  567. public void Draw_WithBorderSubView_At_Col3_In_WideGlyph_DrawsCorrectly ()
  568. {
  569. IApplication app = Application.Create ();
  570. app.Init ("fake");
  571. IDriver driver = app!.Driver!;
  572. driver.SetScreenSize (6, 3); // Screen: 6 cols wide, 3 rows high; enough for 3x3 border subview at col 3 plus content on the left
  573. driver!.Clip = new (driver.Screen);
  574. var superView = new Runnable ()
  575. {
  576. X = 0,
  577. Y = 0,
  578. Width = Dim.Fill (),
  579. Height = Dim.Fill (),
  580. Driver = driver
  581. };
  582. Rune codepoint = Glyphs.Apple;
  583. superView.DrawingContent += (s, e) =>
  584. {
  585. View? view = s as View;
  586. view?.AddStr (0, 0, "🍎🍎🍎🍎");
  587. view?.AddStr (0, 1, "🍎🍎🍎🍎");
  588. view?.AddStr (0, 2, "🍎🍎🍎🍎");
  589. e.DrawContext?.AddDrawnRectangle (view!.Viewport);
  590. e.Cancel = true;
  591. };
  592. // Minimal border at X=3 (odd column), Width=3, Height=3 (includes border)
  593. var viewWithBorder = new View
  594. {
  595. Text = "X",
  596. BorderStyle = LineStyle.Single,
  597. X = 3,
  598. Y = 0,
  599. Width = 3,
  600. Height = 3
  601. };
  602. driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
  603. superView.Add (viewWithBorder);
  604. app.Begin (superView);
  605. DriverAssert.AssertDriverContentsAre (
  606. """
  607. 🍎①┌─┐
  608. 🍎①│X│
  609. 🍎①└─┘
  610. """,
  611. output,
  612. driver);
  613. DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎①┌─┐🍎①│X│🍎①└─┘",
  614. output, driver);
  615. DriverImpl? driverImpl = driver as DriverImpl;
  616. FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
  617. output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
  618. }
  619. [Fact]
  620. public void Draw_NonVisibleView_DoesNotUpdateClip ()
  621. {
  622. IDriver driver = CreateFakeDriver ();
  623. var originalClip = new Region (driver.Screen);
  624. driver.Clip = originalClip.Clone ();
  625. var view = new View
  626. {
  627. X = 10,
  628. Y = 10,
  629. Width = 20,
  630. Height = 20,
  631. Visible = false,
  632. Driver = driver
  633. };
  634. view.BeginInit ();
  635. view.EndInit ();
  636. view.Draw ();
  637. // Clip should not be modified for invisible views
  638. Assert.True (driver.Clip.Equals (originalClip));
  639. }
  640. [Fact]
  641. public void ExcludeFromClip_ExcludesRegion ()
  642. {
  643. IDriver driver = CreateFakeDriver ();
  644. driver.Clip = new (driver.Screen);
  645. var view = new View
  646. {
  647. X = 10,
  648. Y = 10,
  649. Width = 20,
  650. Height = 20,
  651. Driver = driver
  652. };
  653. view.BeginInit ();
  654. view.EndInit ();
  655. view.LayoutSubViews ();
  656. var excludeRect = new Rectangle (15, 15, 10, 10);
  657. view.ExcludeFromClip (excludeRect);
  658. Assert.NotNull (driver.Clip);
  659. Assert.False (driver.Clip.Contains (20, 20)); // Point inside excluded rect should not be in clip
  660. }
  661. [Fact]
  662. public void ExcludeFromClip_WithNullClip_DoesNotThrow ()
  663. {
  664. IDriver driver = CreateFakeDriver ();
  665. driver.Clip = null!;
  666. var view = new View
  667. {
  668. X = 10,
  669. Y = 10,
  670. Width = 20,
  671. Height = 20,
  672. Driver = driver
  673. };
  674. Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (15, 15, 10, 10)));
  675. Assert.Null (exception);
  676. }
  677. #endregion
  678. #region Misc Tests
  679. [Fact]
  680. public void SetClip_SetsDriverClip ()
  681. {
  682. IDriver driver = CreateFakeDriver ();
  683. var view = new View
  684. {
  685. X = 10,
  686. Y = 10,
  687. Width = 20,
  688. Height = 20,
  689. Driver = driver
  690. };
  691. var newClip = new Region (new (5, 5, 30, 30));
  692. view.SetClip (newClip);
  693. Assert.Equal (newClip, driver.Clip);
  694. }
  695. [Fact (Skip = "See BUGBUG in SetClip")]
  696. public void SetClip_WithNullClip_ClearsClip ()
  697. {
  698. IDriver driver = CreateFakeDriver ();
  699. driver.Clip = new (new (10, 10, 20, 20));
  700. var view = new View
  701. {
  702. X = 10,
  703. Y = 10,
  704. Width = 20,
  705. Height = 20,
  706. Driver = driver
  707. };
  708. view.SetClip (null);
  709. Assert.Null (driver.Clip);
  710. }
  711. [Fact]
  712. public void Draw_Excludes_View_From_Clip ()
  713. {
  714. IDriver driver = CreateFakeDriver ();
  715. var originalClip = new Region (driver.Screen);
  716. driver.Clip = originalClip.Clone ();
  717. var view = new View
  718. {
  719. X = 10,
  720. Y = 10,
  721. Width = 20,
  722. Height = 20,
  723. Driver = driver
  724. };
  725. view.BeginInit ();
  726. view.EndInit ();
  727. view.LayoutSubViews ();
  728. Region clipWithViewExcluded = originalClip.Clone ();
  729. clipWithViewExcluded.Exclude (view.Frame);
  730. view.Draw ();
  731. Assert.Equal (clipWithViewExcluded, driver.Clip);
  732. Assert.NotNull (driver.Clip);
  733. }
  734. [Fact]
  735. public void Draw_EmptyViewport_DoesNotCrash ()
  736. {
  737. IDriver driver = CreateFakeDriver ();
  738. driver.Clip = new (driver.Screen);
  739. var view = new View
  740. {
  741. X = 10,
  742. Y = 10,
  743. Width = 1,
  744. Height = 1,
  745. Driver = driver
  746. };
  747. view.Border!.Thickness = new (1);
  748. view.BeginInit ();
  749. view.EndInit ();
  750. view.LayoutSubViews ();
  751. // With border of 1, viewport should be empty (0x0 or negative)
  752. Exception? exception = Record.Exception (() => view.Draw ());
  753. Assert.Null (exception);
  754. }
  755. [Fact]
  756. public void Draw_VeryLargeView_HandlesClippingCorrectly ()
  757. {
  758. IDriver driver = CreateFakeDriver ();
  759. driver.Clip = new (driver.Screen);
  760. var view = new View
  761. {
  762. X = 0,
  763. Y = 0,
  764. Width = 1000,
  765. Height = 1000,
  766. Driver = driver
  767. };
  768. view.BeginInit ();
  769. view.EndInit ();
  770. view.LayoutSubViews ();
  771. Exception? exception = Record.Exception (() => view.Draw ());
  772. Assert.Null (exception);
  773. }
  774. [Fact]
  775. public void Draw_NegativeCoordinates_HandlesClippingCorrectly ()
  776. {
  777. IDriver driver = CreateFakeDriver ();
  778. driver.Clip = new (driver.Screen);
  779. var view = new View
  780. {
  781. X = -10,
  782. Y = -10,
  783. Width = 50,
  784. Height = 50,
  785. Driver = driver
  786. };
  787. view.BeginInit ();
  788. view.EndInit ();
  789. view.LayoutSubViews ();
  790. Exception? exception = Record.Exception (() => view.Draw ());
  791. Assert.Null (exception);
  792. }
  793. [Fact]
  794. public void Draw_OutOfScreenBounds_HandlesClippingCorrectly ()
  795. {
  796. IDriver driver = CreateFakeDriver ();
  797. driver.Clip = new (driver.Screen);
  798. var view = new View
  799. {
  800. X = 100,
  801. Y = 100,
  802. Width = 50,
  803. Height = 50,
  804. Driver = driver
  805. };
  806. view.BeginInit ();
  807. view.EndInit ();
  808. view.LayoutSubViews ();
  809. Exception? exception = Record.Exception (() => view.Draw ());
  810. Assert.Null (exception);
  811. }
  812. #endregion
  813. }