ViewDrawingClippingTests.cs 32 KB

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