@fairyhunter13/opentui-core 0.1.91 → 0.1.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (570) hide show
  1. package/3d/SpriteResourceManager.d.ts +74 -0
  2. package/3d/SpriteUtils.d.ts +13 -0
  3. package/3d/TextureUtils.d.ts +24 -0
  4. package/3d/ThreeRenderable.d.ts +40 -0
  5. package/3d/WGPURenderer.d.ts +61 -0
  6. package/3d/animation/ExplodingSpriteEffect.d.ts +71 -0
  7. package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +76 -0
  8. package/3d/animation/SpriteAnimator.d.ts +124 -0
  9. package/3d/animation/SpriteParticleGenerator.d.ts +62 -0
  10. package/3d/canvas.d.ts +44 -0
  11. package/3d/index.d.ts +12 -0
  12. package/3d/physics/PlanckPhysicsAdapter.d.ts +19 -0
  13. package/3d/physics/RapierPhysicsAdapter.d.ts +19 -0
  14. package/3d/physics/physics-interface.d.ts +27 -0
  15. package/3d.d.ts +2 -0
  16. package/3d.js +34042 -0
  17. package/3d.js.map +155 -0
  18. package/LICENSE +21 -0
  19. package/NativeSpanFeed.d.ts +41 -0
  20. package/Renderable.d.ts +334 -0
  21. package/animation/Timeline.d.ts +126 -0
  22. package/ansi.d.ts +13 -0
  23. package/buffer.d.ts +107 -0
  24. package/console.d.ts +143 -0
  25. package/edit-buffer.d.ts +98 -0
  26. package/editor-view.d.ts +73 -0
  27. package/index-e6ec7apq.js +18415 -0
  28. package/index-e6ec7apq.js.map +64 -0
  29. package/index-h066zmrb.js +12619 -0
  30. package/index-h066zmrb.js.map +43 -0
  31. package/index-ynzawt3n.js +113 -0
  32. package/index-ynzawt3n.js.map +10 -0
  33. package/index.d.ts +21 -0
  34. package/index.js +430 -0
  35. package/index.js.map +9 -0
  36. package/lib/KeyHandler.d.ts +61 -0
  37. package/lib/RGBA.d.ts +25 -0
  38. package/lib/ascii.font.d.ts +508 -0
  39. package/lib/border.d.ts +49 -0
  40. package/lib/bunfs.d.ts +7 -0
  41. package/lib/clipboard.d.ts +17 -0
  42. package/lib/clock.d.ts +15 -0
  43. package/lib/data-paths.d.ts +26 -0
  44. package/lib/debounce.d.ts +42 -0
  45. package/lib/detect-links.d.ts +6 -0
  46. package/lib/env.d.ts +42 -0
  47. package/lib/extmarks-history.d.ts +17 -0
  48. package/lib/extmarks.d.ts +89 -0
  49. package/lib/hast-styled-text.d.ts +17 -0
  50. package/lib/index.d.ts +21 -0
  51. package/lib/keymapping.d.ts +25 -0
  52. package/lib/objects-in-viewport.d.ts +24 -0
  53. package/lib/output.capture.d.ts +24 -0
  54. package/lib/parse.keypress-kitty.d.ts +2 -0
  55. package/lib/parse.keypress.d.ts +26 -0
  56. package/lib/parse.mouse.d.ts +30 -0
  57. package/lib/paste.d.ts +7 -0
  58. package/lib/queue.d.ts +15 -0
  59. package/lib/renderable.validations.d.ts +12 -0
  60. package/lib/scroll-acceleration.d.ts +43 -0
  61. package/lib/selection.d.ts +63 -0
  62. package/lib/singleton.d.ts +7 -0
  63. package/lib/stdin-parser.d.ts +76 -0
  64. package/lib/styled-text.d.ts +63 -0
  65. package/lib/terminal-capability-detection.d.ts +30 -0
  66. package/lib/terminal-palette.d.ts +50 -0
  67. package/lib/tree-sitter/assets/update.d.ts +11 -0
  68. package/lib/tree-sitter/client.d.ts +47 -0
  69. package/lib/tree-sitter/default-parsers.d.ts +2 -0
  70. package/lib/tree-sitter/download-utils.d.ts +21 -0
  71. package/lib/tree-sitter/index.d.ts +8 -0
  72. package/lib/tree-sitter/parser.worker.d.ts +1 -0
  73. package/lib/tree-sitter/parsers-config.d.ts +38 -0
  74. package/lib/tree-sitter/resolve-ft.d.ts +2 -0
  75. package/lib/tree-sitter/types.d.ts +81 -0
  76. package/lib/tree-sitter-styled-text.d.ts +14 -0
  77. package/lib/validate-dir-name.d.ts +1 -0
  78. package/lib/yoga.options.d.ts +32 -0
  79. package/package.json +51 -63
  80. package/parser.worker.js +869 -0
  81. package/parser.worker.js.map +12 -0
  82. package/plugins/core-slot.d.ts +72 -0
  83. package/plugins/registry.d.ts +38 -0
  84. package/plugins/types.d.ts +34 -0
  85. package/post/filters.d.ts +105 -0
  86. package/renderables/ASCIIFont.d.ts +52 -0
  87. package/renderables/Box.d.ts +72 -0
  88. package/renderables/Code.d.ts +78 -0
  89. package/renderables/Diff.d.ts +142 -0
  90. package/renderables/EditBufferRenderable.d.ts +162 -0
  91. package/renderables/FrameBuffer.d.ts +16 -0
  92. package/renderables/Input.d.ts +67 -0
  93. package/renderables/LineNumberRenderable.d.ts +74 -0
  94. package/renderables/Markdown.d.ts +173 -0
  95. package/renderables/ScrollBar.d.ts +77 -0
  96. package/renderables/ScrollBox.d.ts +124 -0
  97. package/renderables/Select.d.ts +115 -0
  98. package/renderables/Slider.d.ts +44 -0
  99. package/renderables/TabSelect.d.ts +96 -0
  100. package/renderables/Text.d.ts +36 -0
  101. package/renderables/TextBufferRenderable.d.ts +105 -0
  102. package/renderables/TextNode.d.ts +91 -0
  103. package/renderables/TextTable.d.ts +140 -0
  104. package/renderables/Textarea.d.ts +114 -0
  105. package/renderables/TimeToFirstDraw.d.ts +24 -0
  106. package/renderables/__tests__/renderable-test-utils.d.ts +12 -0
  107. package/renderables/composition/VRenderable.d.ts +16 -0
  108. package/renderables/composition/constructs.d.ts +35 -0
  109. package/renderables/composition/vnode.d.ts +46 -0
  110. package/renderables/index.d.ts +22 -0
  111. package/renderables/markdown-parser.d.ts +10 -0
  112. package/renderer.d.ts +388 -0
  113. package/runtime-plugin-support.d.ts +3 -0
  114. package/runtime-plugin-support.js +29 -0
  115. package/runtime-plugin-support.js.map +10 -0
  116. package/runtime-plugin.d.ts +11 -0
  117. package/runtime-plugin.js +16 -0
  118. package/runtime-plugin.js.map +9 -0
  119. package/syntax-style.d.ts +54 -0
  120. package/testing/manual-clock.d.ts +16 -0
  121. package/testing/mock-keys.d.ts +81 -0
  122. package/testing/mock-mouse.d.ts +38 -0
  123. package/testing/mock-tree-sitter-client.d.ts +23 -0
  124. package/testing/spy.d.ts +7 -0
  125. package/testing/test-recorder.d.ts +61 -0
  126. package/testing/test-renderer.d.ts +23 -0
  127. package/testing.d.ts +6 -0
  128. package/testing.js +675 -0
  129. package/testing.js.map +15 -0
  130. package/text-buffer-view.d.ts +42 -0
  131. package/text-buffer.d.ts +67 -0
  132. package/types.d.ts +131 -0
  133. package/utils.d.ts +14 -0
  134. package/zig-structs.d.ts +155 -0
  135. package/zig.d.ts +351 -0
  136. package/dev/keypress-debug-renderer.ts +0 -148
  137. package/dev/keypress-debug.ts +0 -43
  138. package/dev/print-env-vars.ts +0 -32
  139. package/dev/test-tmux-graphics-334.sh +0 -68
  140. package/dev/thai-debug-test.ts +0 -68
  141. package/docs/development.md +0 -141
  142. package/docs/env-vars.md +0 -140
  143. package/docs/getting-started.md +0 -353
  144. package/docs/renderables-vs-constructs.md +0 -159
  145. package/docs/tree-sitter.md +0 -311
  146. package/scripts/build.ts +0 -400
  147. package/scripts/publish.ts +0 -60
  148. package/src/3d/SpriteResourceManager.ts +0 -286
  149. package/src/3d/SpriteUtils.ts +0 -71
  150. package/src/3d/TextureUtils.ts +0 -196
  151. package/src/3d/ThreeRenderable.ts +0 -197
  152. package/src/3d/WGPURenderer.ts +0 -294
  153. package/src/3d/animation/ExplodingSpriteEffect.ts +0 -513
  154. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +0 -429
  155. package/src/3d/animation/SpriteAnimator.ts +0 -633
  156. package/src/3d/animation/SpriteParticleGenerator.ts +0 -435
  157. package/src/3d/canvas.ts +0 -464
  158. package/src/3d/index.ts +0 -12
  159. package/src/3d/physics/PlanckPhysicsAdapter.ts +0 -72
  160. package/src/3d/physics/RapierPhysicsAdapter.ts +0 -66
  161. package/src/3d/physics/physics-interface.ts +0 -31
  162. package/src/3d/shaders/supersampling.wgsl +0 -201
  163. package/src/3d.ts +0 -3
  164. package/src/NativeSpanFeed.ts +0 -300
  165. package/src/Renderable.ts +0 -1698
  166. package/src/__snapshots__/buffer.test.ts.snap +0 -28
  167. package/src/animation/Timeline.test.ts +0 -2709
  168. package/src/animation/Timeline.ts +0 -598
  169. package/src/ansi.ts +0 -18
  170. package/src/benchmark/latest-all-bench-run.json +0 -707
  171. package/src/benchmark/latest-async-bench-run.json +0 -336
  172. package/src/benchmark/latest-default-bench-run.json +0 -657
  173. package/src/benchmark/latest-large-bench-run.json +0 -707
  174. package/src/benchmark/latest-quick-bench-run.json +0 -207
  175. package/src/benchmark/markdown-benchmark.ts +0 -1804
  176. package/src/benchmark/native-span-feed-async-benchmark.ts +0 -355
  177. package/src/benchmark/native-span-feed-benchmark.md +0 -56
  178. package/src/benchmark/native-span-feed-benchmark.ts +0 -596
  179. package/src/benchmark/native-span-feed-compare.ts +0 -280
  180. package/src/benchmark/renderer-benchmark.ts +0 -754
  181. package/src/benchmark/text-table-benchmark.ts +0 -947
  182. package/src/buffer.test.ts +0 -291
  183. package/src/buffer.ts +0 -519
  184. package/src/console.test.ts +0 -612
  185. package/src/console.ts +0 -1255
  186. package/src/edit-buffer.test.ts +0 -1769
  187. package/src/edit-buffer.ts +0 -411
  188. package/src/editor-view.test.ts +0 -1032
  189. package/src/editor-view.ts +0 -284
  190. package/src/examples/ascii-font-selection-demo.ts +0 -245
  191. package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
  192. package/src/examples/assets/concrete.png +0 -0
  193. package/src/examples/assets/crate.png +0 -0
  194. package/src/examples/assets/crate_emissive.png +0 -0
  195. package/src/examples/assets/forrest_background.png +0 -0
  196. package/src/examples/assets/hast-example.json +0 -1018
  197. package/src/examples/assets/heart.png +0 -0
  198. package/src/examples/assets/main_char_heavy_attack.png +0 -0
  199. package/src/examples/assets/main_char_idle.png +0 -0
  200. package/src/examples/assets/main_char_jump_end.png +0 -0
  201. package/src/examples/assets/main_char_jump_landing.png +0 -0
  202. package/src/examples/assets/main_char_jump_start.png +0 -0
  203. package/src/examples/assets/main_char_run_loop.png +0 -0
  204. package/src/examples/assets/roughness_map.jpg +0 -0
  205. package/src/examples/build.ts +0 -115
  206. package/src/examples/code-demo.ts +0 -584
  207. package/src/examples/console-demo.ts +0 -358
  208. package/src/examples/core-plugin-slots-demo.ts +0 -759
  209. package/src/examples/diff-demo.ts +0 -699
  210. package/src/examples/draggable-three-demo.ts +0 -259
  211. package/src/examples/editor-demo.ts +0 -322
  212. package/src/examples/extmarks-demo.ts +0 -204
  213. package/src/examples/focus-restore-demo.ts +0 -310
  214. package/src/examples/fonts.ts +0 -245
  215. package/src/examples/fractal-shader-demo.ts +0 -268
  216. package/src/examples/framebuffer-demo.ts +0 -674
  217. package/src/examples/full-unicode-demo.ts +0 -181
  218. package/src/examples/golden-star-demo.ts +0 -933
  219. package/src/examples/grayscale-buffer-demo.ts +0 -249
  220. package/src/examples/hast-syntax-highlighting-demo.ts +0 -129
  221. package/src/examples/index.ts +0 -925
  222. package/src/examples/input-demo.ts +0 -377
  223. package/src/examples/input-select-layout-demo.ts +0 -425
  224. package/src/examples/install.sh +0 -143
  225. package/src/examples/keypress-debug-demo.ts +0 -452
  226. package/src/examples/lib/HexList.ts +0 -122
  227. package/src/examples/lib/PaletteGrid.ts +0 -125
  228. package/src/examples/lib/standalone-keys.ts +0 -25
  229. package/src/examples/lib/tab-controller.ts +0 -243
  230. package/src/examples/lights-phong-demo.ts +0 -290
  231. package/src/examples/link-demo.ts +0 -220
  232. package/src/examples/live-state-demo.ts +0 -480
  233. package/src/examples/markdown-demo.ts +0 -620
  234. package/src/examples/mouse-interaction-demo.ts +0 -428
  235. package/src/examples/nested-zindex-demo.ts +0 -357
  236. package/src/examples/opacity-example.ts +0 -235
  237. package/src/examples/opentui-demo.ts +0 -1057
  238. package/src/examples/physx-planck-2d-demo.ts +0 -507
  239. package/src/examples/physx-rapier-2d-demo.ts +0 -526
  240. package/src/examples/relative-positioning-demo.ts +0 -323
  241. package/src/examples/scroll-example.ts +0 -214
  242. package/src/examples/scrollbox-mouse-test.ts +0 -112
  243. package/src/examples/scrollbox-overlay-hit-test.ts +0 -206
  244. package/src/examples/select-demo.ts +0 -237
  245. package/src/examples/shader-cube-demo.ts +0 -772
  246. package/src/examples/simple-layout-example.ts +0 -591
  247. package/src/examples/slider-demo.ts +0 -617
  248. package/src/examples/split-mode-demo.ts +0 -445
  249. package/src/examples/sprite-animation-demo.ts +0 -443
  250. package/src/examples/sprite-particle-generator-demo.ts +0 -486
  251. package/src/examples/static-sprite-demo.ts +0 -193
  252. package/src/examples/sticky-scroll-example.ts +0 -308
  253. package/src/examples/styled-text-demo.ts +0 -282
  254. package/src/examples/tab-select-demo.ts +0 -219
  255. package/src/examples/terminal-title.ts +0 -29
  256. package/src/examples/terminal.ts +0 -305
  257. package/src/examples/text-node-demo.ts +0 -416
  258. package/src/examples/text-selection-demo.ts +0 -377
  259. package/src/examples/text-table-demo.ts +0 -503
  260. package/src/examples/text-truncation-demo.ts +0 -481
  261. package/src/examples/text-wrap.ts +0 -757
  262. package/src/examples/texture-loading-demo.ts +0 -259
  263. package/src/examples/timeline-example.ts +0 -670
  264. package/src/examples/transparency-demo.ts +0 -241
  265. package/src/examples/vnode-composition-demo.ts +0 -404
  266. package/src/index.ts +0 -22
  267. package/src/lib/KeyHandler.integration.test.ts +0 -292
  268. package/src/lib/KeyHandler.stopPropagation.test.ts +0 -289
  269. package/src/lib/KeyHandler.test.ts +0 -662
  270. package/src/lib/KeyHandler.ts +0 -222
  271. package/src/lib/RGBA.test.ts +0 -984
  272. package/src/lib/RGBA.ts +0 -204
  273. package/src/lib/ascii.font.ts +0 -330
  274. package/src/lib/border.test.ts +0 -83
  275. package/src/lib/border.ts +0 -168
  276. package/src/lib/bunfs.test.ts +0 -27
  277. package/src/lib/bunfs.ts +0 -18
  278. package/src/lib/clipboard.test.ts +0 -41
  279. package/src/lib/clipboard.ts +0 -47
  280. package/src/lib/clock.ts +0 -31
  281. package/src/lib/data-paths.test.ts +0 -133
  282. package/src/lib/data-paths.ts +0 -109
  283. package/src/lib/debounce.ts +0 -106
  284. package/src/lib/detect-links.test.ts +0 -98
  285. package/src/lib/detect-links.ts +0 -56
  286. package/src/lib/env.test.ts +0 -228
  287. package/src/lib/env.ts +0 -209
  288. package/src/lib/extmarks-history.ts +0 -51
  289. package/src/lib/extmarks-multiwidth.test.ts +0 -322
  290. package/src/lib/extmarks.test.ts +0 -3457
  291. package/src/lib/extmarks.ts +0 -843
  292. package/src/lib/fonts/block.json +0 -405
  293. package/src/lib/fonts/grid.json +0 -265
  294. package/src/lib/fonts/huge.json +0 -741
  295. package/src/lib/fonts/pallet.json +0 -314
  296. package/src/lib/fonts/shade.json +0 -591
  297. package/src/lib/fonts/slick.json +0 -321
  298. package/src/lib/fonts/tiny.json +0 -69
  299. package/src/lib/hast-styled-text.ts +0 -59
  300. package/src/lib/index.ts +0 -21
  301. package/src/lib/keymapping.test.ts +0 -280
  302. package/src/lib/keymapping.ts +0 -87
  303. package/src/lib/objects-in-viewport.test.ts +0 -787
  304. package/src/lib/objects-in-viewport.ts +0 -153
  305. package/src/lib/output.capture.ts +0 -58
  306. package/src/lib/parse.keypress-kitty.protocol.test.ts +0 -340
  307. package/src/lib/parse.keypress-kitty.test.ts +0 -663
  308. package/src/lib/parse.keypress-kitty.ts +0 -439
  309. package/src/lib/parse.keypress.test.ts +0 -1849
  310. package/src/lib/parse.keypress.ts +0 -397
  311. package/src/lib/parse.mouse.test.ts +0 -552
  312. package/src/lib/parse.mouse.ts +0 -232
  313. package/src/lib/paste.ts +0 -16
  314. package/src/lib/queue.ts +0 -65
  315. package/src/lib/renderable.validations.test.ts +0 -87
  316. package/src/lib/renderable.validations.ts +0 -83
  317. package/src/lib/scroll-acceleration.ts +0 -98
  318. package/src/lib/selection.ts +0 -240
  319. package/src/lib/singleton.ts +0 -28
  320. package/src/lib/stdin-parser.test.ts +0 -1676
  321. package/src/lib/stdin-parser.ts +0 -1248
  322. package/src/lib/styled-text.ts +0 -178
  323. package/src/lib/terminal-capability-detection.test.ts +0 -202
  324. package/src/lib/terminal-capability-detection.ts +0 -79
  325. package/src/lib/terminal-palette.test.ts +0 -878
  326. package/src/lib/terminal-palette.ts +0 -383
  327. package/src/lib/tree-sitter/assets/README.md +0 -118
  328. package/src/lib/tree-sitter/assets/update.ts +0 -331
  329. package/src/lib/tree-sitter/assets.d.ts +0 -9
  330. package/src/lib/tree-sitter/cache.test.ts +0 -270
  331. package/src/lib/tree-sitter/client.test.ts +0 -1061
  332. package/src/lib/tree-sitter/client.ts +0 -615
  333. package/src/lib/tree-sitter/default-parsers.ts +0 -80
  334. package/src/lib/tree-sitter/download-utils.ts +0 -148
  335. package/src/lib/tree-sitter/index.ts +0 -28
  336. package/src/lib/tree-sitter/parser.worker.ts +0 -1001
  337. package/src/lib/tree-sitter/parsers-config.ts +0 -75
  338. package/src/lib/tree-sitter/resolve-ft.ts +0 -62
  339. package/src/lib/tree-sitter/types.ts +0 -81
  340. package/src/lib/tree-sitter-styled-text.test.ts +0 -1253
  341. package/src/lib/tree-sitter-styled-text.ts +0 -306
  342. package/src/lib/validate-dir-name.ts +0 -55
  343. package/src/lib/yoga.options.test.ts +0 -628
  344. package/src/lib/yoga.options.ts +0 -346
  345. package/src/plugins/core-slot.ts +0 -579
  346. package/src/plugins/registry.ts +0 -377
  347. package/src/plugins/types.ts +0 -46
  348. package/src/post/filters.ts +0 -888
  349. package/src/renderables/ASCIIFont.ts +0 -219
  350. package/src/renderables/Box.test.ts +0 -160
  351. package/src/renderables/Box.ts +0 -295
  352. package/src/renderables/Code.test.ts +0 -2062
  353. package/src/renderables/Code.ts +0 -357
  354. package/src/renderables/Diff.regression.test.ts +0 -226
  355. package/src/renderables/Diff.test.ts +0 -3027
  356. package/src/renderables/Diff.ts +0 -1209
  357. package/src/renderables/EditBufferRenderable.ts +0 -764
  358. package/src/renderables/FrameBuffer.ts +0 -47
  359. package/src/renderables/Input.test.ts +0 -1228
  360. package/src/renderables/Input.ts +0 -245
  361. package/src/renderables/LineNumberRenderable.ts +0 -675
  362. package/src/renderables/Markdown.ts +0 -1106
  363. package/src/renderables/ScrollBar.ts +0 -422
  364. package/src/renderables/ScrollBox.ts +0 -883
  365. package/src/renderables/Select.test.ts +0 -1010
  366. package/src/renderables/Select.ts +0 -523
  367. package/src/renderables/Slider.test.ts +0 -456
  368. package/src/renderables/Slider.ts +0 -347
  369. package/src/renderables/TabSelect.test.ts +0 -197
  370. package/src/renderables/TabSelect.ts +0 -455
  371. package/src/renderables/Text.selection-buffer.test.ts +0 -123
  372. package/src/renderables/Text.test.ts +0 -2660
  373. package/src/renderables/Text.ts +0 -147
  374. package/src/renderables/TextBufferRenderable.ts +0 -518
  375. package/src/renderables/TextNode.test.ts +0 -1058
  376. package/src/renderables/TextNode.ts +0 -325
  377. package/src/renderables/TextTable.test.ts +0 -1421
  378. package/src/renderables/TextTable.ts +0 -1344
  379. package/src/renderables/Textarea.ts +0 -732
  380. package/src/renderables/TimeToFirstDraw.ts +0 -89
  381. package/src/renderables/__snapshots__/Code.test.ts.snap +0 -13
  382. package/src/renderables/__snapshots__/Diff.test.ts.snap +0 -785
  383. package/src/renderables/__snapshots__/Text.test.ts.snap +0 -421
  384. package/src/renderables/__snapshots__/TextTable.test.ts.snap +0 -215
  385. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +0 -144
  386. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +0 -816
  387. package/src/renderables/__tests__/LineNumberRenderable.test.ts +0 -1787
  388. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +0 -85
  389. package/src/renderables/__tests__/Markdown.test.ts +0 -2287
  390. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +0 -87
  391. package/src/renderables/__tests__/Textarea.buffer.test.ts +0 -682
  392. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +0 -675
  393. package/src/renderables/__tests__/Textarea.editing.test.ts +0 -2041
  394. package/src/renderables/__tests__/Textarea.error-handling.test.ts +0 -35
  395. package/src/renderables/__tests__/Textarea.events.test.ts +0 -738
  396. package/src/renderables/__tests__/Textarea.highlights.test.ts +0 -590
  397. package/src/renderables/__tests__/Textarea.keybinding.test.ts +0 -3149
  398. package/src/renderables/__tests__/Textarea.paste.test.ts +0 -357
  399. package/src/renderables/__tests__/Textarea.rendering.test.ts +0 -1864
  400. package/src/renderables/__tests__/Textarea.scroll.test.ts +0 -733
  401. package/src/renderables/__tests__/Textarea.selection.test.ts +0 -1590
  402. package/src/renderables/__tests__/Textarea.stress.test.ts +0 -670
  403. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +0 -383
  404. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +0 -310
  405. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +0 -221
  406. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +0 -89
  407. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +0 -457
  408. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +0 -158
  409. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +0 -387
  410. package/src/renderables/__tests__/markdown-parser.test.ts +0 -217
  411. package/src/renderables/__tests__/renderable-test-utils.ts +0 -60
  412. package/src/renderables/composition/README.md +0 -8
  413. package/src/renderables/composition/VRenderable.ts +0 -32
  414. package/src/renderables/composition/constructs.ts +0 -127
  415. package/src/renderables/composition/vnode.ts +0 -289
  416. package/src/renderables/index.ts +0 -22
  417. package/src/renderables/markdown-parser.ts +0 -66
  418. package/src/renderer.ts +0 -2363
  419. package/src/runtime-plugin-support.ts +0 -39
  420. package/src/runtime-plugin.ts +0 -144
  421. package/src/syntax-style.test.ts +0 -841
  422. package/src/syntax-style.ts +0 -264
  423. package/src/testing/README.md +0 -210
  424. package/src/testing/capture-spans.test.ts +0 -194
  425. package/src/testing/integration.test.ts +0 -276
  426. package/src/testing/manual-clock.ts +0 -106
  427. package/src/testing/mock-keys.test.ts +0 -1356
  428. package/src/testing/mock-keys.ts +0 -449
  429. package/src/testing/mock-mouse.test.ts +0 -218
  430. package/src/testing/mock-mouse.ts +0 -247
  431. package/src/testing/mock-tree-sitter-client.ts +0 -73
  432. package/src/testing/spy.ts +0 -13
  433. package/src/testing/test-recorder.test.ts +0 -415
  434. package/src/testing/test-recorder.ts +0 -145
  435. package/src/testing/test-renderer.ts +0 -116
  436. package/src/testing.ts +0 -7
  437. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +0 -481
  438. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +0 -19
  439. package/src/tests/__snapshots__/scrollbox.test.ts.snap +0 -29
  440. package/src/tests/absolute-positioning.snapshot.test.ts +0 -638
  441. package/src/tests/allocator-stats.test.ts +0 -38
  442. package/src/tests/destroy-during-render.test.ts +0 -200
  443. package/src/tests/hover-cursor.test.ts +0 -98
  444. package/src/tests/native-span-feed-async.test.ts +0 -173
  445. package/src/tests/native-span-feed-close.test.ts +0 -120
  446. package/src/tests/native-span-feed-coverage.test.ts +0 -227
  447. package/src/tests/native-span-feed-edge-cases.test.ts +0 -352
  448. package/src/tests/native-span-feed-use-after-free.test.ts +0 -45
  449. package/src/tests/opacity.test.ts +0 -123
  450. package/src/tests/renderable.snapshot.test.ts +0 -524
  451. package/src/tests/renderable.test.ts +0 -1281
  452. package/src/tests/renderer.console-startup.test.ts +0 -65
  453. package/src/tests/renderer.control.test.ts +0 -364
  454. package/src/tests/renderer.core-slot-binding.test.ts +0 -952
  455. package/src/tests/renderer.cursor.test.ts +0 -26
  456. package/src/tests/renderer.destroy-during-render.test.ts +0 -110
  457. package/src/tests/renderer.focus-restore.test.ts +0 -228
  458. package/src/tests/renderer.focus.test.ts +0 -251
  459. package/src/tests/renderer.idle.test.ts +0 -219
  460. package/src/tests/renderer.input.test.ts +0 -2145
  461. package/src/tests/renderer.kitty-flags.test.ts +0 -195
  462. package/src/tests/renderer.mouse.test.ts +0 -1269
  463. package/src/tests/renderer.palette.test.ts +0 -629
  464. package/src/tests/renderer.selection.test.ts +0 -49
  465. package/src/tests/renderer.slot-registry.test.ts +0 -649
  466. package/src/tests/renderer.useMouse.test.ts +0 -50
  467. package/src/tests/runtime-plugin-support.fixture.ts +0 -11
  468. package/src/tests/runtime-plugin-support.test.ts +0 -28
  469. package/src/tests/runtime-plugin.fixture.ts +0 -40
  470. package/src/tests/runtime-plugin.test.ts +0 -190
  471. package/src/tests/scrollbox-culling-bug.test.ts +0 -114
  472. package/src/tests/scrollbox-hitgrid-resize.test.ts +0 -136
  473. package/src/tests/scrollbox-hitgrid.test.ts +0 -909
  474. package/src/tests/scrollbox.test.ts +0 -1530
  475. package/src/tests/wrap-resize-perf.test.ts +0 -229
  476. package/src/tests/yoga-setters.test.ts +0 -921
  477. package/src/text-buffer-view.test.ts +0 -705
  478. package/src/text-buffer-view.ts +0 -189
  479. package/src/text-buffer.test.ts +0 -347
  480. package/src/text-buffer.ts +0 -250
  481. package/src/types.ts +0 -152
  482. package/src/utils.ts +0 -88
  483. package/src/zig/ansi.zig +0 -268
  484. package/src/zig/bench/README.md +0 -50
  485. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +0 -887
  486. package/src/zig/bench/edit-buffer_bench.zig +0 -476
  487. package/src/zig/bench/native-span-feed_bench.zig +0 -100
  488. package/src/zig/bench/rope-markers_bench.zig +0 -713
  489. package/src/zig/bench/rope_bench.zig +0 -514
  490. package/src/zig/bench/styled-text_bench.zig +0 -470
  491. package/src/zig/bench/text-buffer-coords_bench.zig +0 -362
  492. package/src/zig/bench/text-buffer-view_bench.zig +0 -459
  493. package/src/zig/bench/text-chunk-graphemes_bench.zig +0 -273
  494. package/src/zig/bench/utf8_bench.zig +0 -799
  495. package/src/zig/bench-utils.zig +0 -431
  496. package/src/zig/bench.zig +0 -217
  497. package/src/zig/buffer.zig +0 -2223
  498. package/src/zig/build.zig +0 -289
  499. package/src/zig/build.zig.zon +0 -16
  500. package/src/zig/edit-buffer.zig +0 -825
  501. package/src/zig/editor-view.zig +0 -802
  502. package/src/zig/event-bus.zig +0 -13
  503. package/src/zig/event-emitter.zig +0 -65
  504. package/src/zig/file-logger.zig +0 -92
  505. package/src/zig/grapheme.zig +0 -599
  506. package/src/zig/lib.zig +0 -1834
  507. package/src/zig/link.zig +0 -333
  508. package/src/zig/logger.zig +0 -43
  509. package/src/zig/mem-registry.zig +0 -125
  510. package/src/zig/native-span-feed-bench-lib.zig +0 -7
  511. package/src/zig/native-span-feed.zig +0 -708
  512. package/src/zig/renderer.zig +0 -1386
  513. package/src/zig/rope.zig +0 -1220
  514. package/src/zig/syntax-style.zig +0 -161
  515. package/src/zig/terminal.zig +0 -975
  516. package/src/zig/test.zig +0 -70
  517. package/src/zig/tests/README.md +0 -18
  518. package/src/zig/tests/buffer_test.zig +0 -2526
  519. package/src/zig/tests/edit-buffer-history_test.zig +0 -271
  520. package/src/zig/tests/edit-buffer_test.zig +0 -1689
  521. package/src/zig/tests/editor-view_test.zig +0 -3299
  522. package/src/zig/tests/event-emitter_test.zig +0 -249
  523. package/src/zig/tests/grapheme_test.zig +0 -1304
  524. package/src/zig/tests/link_test.zig +0 -190
  525. package/src/zig/tests/mem-registry_test.zig +0 -473
  526. package/src/zig/tests/memory_leak_regression_test.zig +0 -159
  527. package/src/zig/tests/native-span-feed_test.zig +0 -1264
  528. package/src/zig/tests/renderer_test.zig +0 -1010
  529. package/src/zig/tests/rope-nested_test.zig +0 -712
  530. package/src/zig/tests/rope_fuzz_test.zig +0 -238
  531. package/src/zig/tests/rope_test.zig +0 -2362
  532. package/src/zig/tests/segment-merge.test.zig +0 -148
  533. package/src/zig/tests/syntax-style_test.zig +0 -557
  534. package/src/zig/tests/terminal_test.zig +0 -719
  535. package/src/zig/tests/text-buffer-drawing_test.zig +0 -3237
  536. package/src/zig/tests/text-buffer-highlights_test.zig +0 -666
  537. package/src/zig/tests/text-buffer-iterators_test.zig +0 -776
  538. package/src/zig/tests/text-buffer-segment_test.zig +0 -320
  539. package/src/zig/tests/text-buffer-selection_test.zig +0 -1035
  540. package/src/zig/tests/text-buffer-selection_viewport_test.zig +0 -358
  541. package/src/zig/tests/text-buffer-view_test.zig +0 -3649
  542. package/src/zig/tests/text-buffer_test.zig +0 -2191
  543. package/src/zig/tests/unicode-width-map.zon +0 -3909
  544. package/src/zig/tests/utf8_no_zwj_test.zig +0 -260
  545. package/src/zig/tests/utf8_test.zig +0 -4057
  546. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +0 -267
  547. package/src/zig/tests/utf8_wcwidth_test.zig +0 -357
  548. package/src/zig/tests/word-wrap-editing_test.zig +0 -498
  549. package/src/zig/tests/wrap-cache-perf_test.zig +0 -113
  550. package/src/zig/text-buffer-iterators.zig +0 -499
  551. package/src/zig/text-buffer-segment.zig +0 -404
  552. package/src/zig/text-buffer-view.zig +0 -1371
  553. package/src/zig/text-buffer.zig +0 -1180
  554. package/src/zig/utf8.zig +0 -1948
  555. package/src/zig/utils.zig +0 -9
  556. package/src/zig-structs.ts +0 -261
  557. package/src/zig.ts +0 -3843
  558. package/tsconfig.build.json +0 -22
  559. package/tsconfig.json +0 -28
  560. /package/{src/lib/tree-sitter/assets → assets}/javascript/highlights.scm +0 -0
  561. /package/{src/lib/tree-sitter/assets → assets}/javascript/tree-sitter-javascript.wasm +0 -0
  562. /package/{src/lib/tree-sitter/assets → assets}/markdown/highlights.scm +0 -0
  563. /package/{src/lib/tree-sitter/assets → assets}/markdown/injections.scm +0 -0
  564. /package/{src/lib/tree-sitter/assets → assets}/markdown/tree-sitter-markdown.wasm +0 -0
  565. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/highlights.scm +0 -0
  566. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  567. /package/{src/lib/tree-sitter/assets → assets}/typescript/highlights.scm +0 -0
  568. /package/{src/lib/tree-sitter/assets → assets}/typescript/tree-sitter-typescript.wasm +0 -0
  569. /package/{src/lib/tree-sitter/assets → assets}/zig/highlights.scm +0 -0
  570. /package/{src/lib/tree-sitter/assets → assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -1,2660 +0,0 @@
1
- import { describe, expect, it, beforeEach, afterEach } from "bun:test"
2
- import { TextRenderable, type TextOptions } from "./Text.js"
3
- import { TextNodeRenderable } from "./TextNode.js"
4
- import { RGBA } from "../lib/RGBA.js"
5
- import { stringToStyledText, StyledText } from "../lib/styled-text.js"
6
- import { createTestRenderer, type MockMouse, type TestRenderer } from "../testing/test-renderer.js"
7
- import { BoxRenderable } from "./Box.js"
8
-
9
- let currentRenderer: TestRenderer
10
- let renderOnce: () => Promise<void>
11
- let currentMouse: MockMouse
12
- let captureFrame: () => string
13
- let resize: (width: number, height: number) => void
14
-
15
- async function createTextRenderable(
16
- renderer: TestRenderer,
17
- options: TextOptions,
18
- ): Promise<{ text: TextRenderable; root: any }> {
19
- const textRenderable = new TextRenderable(renderer, { left: 0, top: 0, ...options })
20
- renderer.root.add(textRenderable)
21
- await renderOnce()
22
-
23
- return { text: textRenderable, root: renderer.root }
24
- }
25
-
26
- describe("TextRenderable Selection", () => {
27
- describe("Native getSelectedText", () => {
28
- it("should use native implementation", async () => {
29
- const { text } = await createTextRenderable(currentRenderer, {
30
- content: "Hello World",
31
- selectable: true,
32
- })
33
-
34
- await currentMouse.drag(text.x, text.y, text.x + 5, text.y)
35
- await renderOnce()
36
-
37
- const selectedText = text.getSelectedText()
38
- expect(selectedText).toBe("Hello")
39
- })
40
-
41
- it("should handle graphemes correctly", async () => {
42
- const { text } = await createTextRenderable(currentRenderer, {
43
- content: "Hello 🌍 World",
44
- selectable: true,
45
- })
46
-
47
- // Select "Hello 🌍" (7 characters: H,e,l,l,o, ,🌍)
48
- await currentMouse.drag(text.x, text.y, text.x + 7, text.y)
49
- await renderOnce()
50
-
51
- const selectedText = text.getSelectedText()
52
- expect(selectedText).toBe("Hello 🌍")
53
- })
54
- })
55
-
56
- beforeEach(async () => {
57
- ;({
58
- renderer: currentRenderer,
59
- renderOnce,
60
- mockMouse: currentMouse,
61
- captureCharFrame: captureFrame,
62
- resize,
63
- } = await createTestRenderer({
64
- width: 20,
65
- height: 5,
66
- }))
67
- })
68
-
69
- afterEach(() => {
70
- currentRenderer.destroy()
71
- })
72
-
73
- describe("Initialization", () => {
74
- it("should initialize properly", async () => {
75
- const { text } = await createTextRenderable(currentRenderer, {
76
- content: "Hello World",
77
- selectable: true,
78
- })
79
-
80
- expect(text.x).toBeDefined()
81
- expect(text.y).toBeDefined()
82
- expect(text.width).toBeGreaterThan(0)
83
- expect(text.height).toBeGreaterThan(0)
84
- })
85
- })
86
-
87
- describe("Basic Selection Flow", () => {
88
- it("should handle selection from start to end", async () => {
89
- const { text } = await createTextRenderable(currentRenderer, {
90
- content: "Hello World",
91
- selectable: true,
92
- })
93
-
94
- expect(text.hasSelection()).toBe(false)
95
- expect(text.getSelection()).toBe(null)
96
- expect(text.getSelectedText()).toBe("")
97
-
98
- expect(text.shouldStartSelection(6, 0)).toBe(true)
99
-
100
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
101
- await renderOnce()
102
-
103
- expect(text.hasSelection()).toBe(true)
104
-
105
- const selection = text.getSelection()
106
- expect(selection).not.toBe(null)
107
- expect(selection!.start).toBe(6)
108
- expect(selection!.end).toBe(11)
109
-
110
- expect(text.getSelectedText()).toBe("World")
111
- })
112
-
113
- it("should handle selection with newline characters", async () => {
114
- const { text } = await createTextRenderable(currentRenderer, {
115
- content: "Line 1\nLine 2\nLine 3",
116
- selectable: true,
117
- })
118
-
119
- // Select from middle of line 2 to middle of line 3
120
- await currentMouse.drag(text.x + 2, text.y + 1, text.x + 4, text.y + 2)
121
- await renderOnce()
122
-
123
- const selection = text.getSelection()
124
- expect(selection).not.toBe(null)
125
- // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 starts at 7
126
- // Position "n" in "Line 2" is at 7 + 2 = 9
127
- expect(selection!.start).toBe(9)
128
- // Line 2 starts at 14, position after "Line" is 14 + 4 = 18
129
- expect(selection!.end).toBe(18)
130
-
131
- expect(text.getSelectedText()).toBe("ne 2\nLine")
132
- })
133
-
134
- it("should handle selection across empty lines", async () => {
135
- const { text } = await createTextRenderable(currentRenderer, {
136
- content: "Line 1\nLine 2\n\nLine 4",
137
- selectable: true,
138
- })
139
-
140
- // Select from start of line 1 to position 2 on empty line 3
141
- await currentMouse.drag(text.x, text.y, text.x + 2, text.y + 2)
142
- await renderOnce()
143
-
144
- const selection = text.getSelection()
145
- expect(selection).not.toBe(null)
146
- // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 (7-12) + newline (13) + Line 2 empty (14)
147
- // Selecting to (col=2, row=2) on empty line clamps to col=0, so end=14
148
- expect(selection!.start).toBe(0)
149
- expect(selection!.end).toBe(14)
150
- expect(text.getSelectedText()).toBe("Line 1\nLine 2")
151
- })
152
-
153
- it("should handle selection ending in empty line", async () => {
154
- const { text } = await createTextRenderable(currentRenderer, {
155
- content: "Line 1\n\nLine 3",
156
- selectable: true,
157
- })
158
-
159
- // Select from start of line 1 into the empty line 2
160
- await currentMouse.drag(text.x, text.y, text.x + 3, text.y + 1)
161
- await renderOnce()
162
-
163
- const selection = text.getSelection()
164
- expect(selection).not.toBe(null)
165
- // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 empty (7)
166
- // Selecting to (col=3, row=1) on empty line clamps to col=0, so end=7
167
- expect(selection!.start).toBe(0)
168
- expect(selection!.end).toBe(7)
169
- expect(text.getSelectedText()).toBe("Line 1")
170
- })
171
-
172
- it("should handle selection spanning multiple lines completely", async () => {
173
- const { text } = await createTextRenderable(currentRenderer, {
174
- content: "First\nSecond\nThird",
175
- selectable: true,
176
- })
177
-
178
- // Select from start of line 1 to end of line 2 (actually selecting Second)
179
- await currentMouse.drag(text.x, text.y + 1, text.x + 6, text.y + 1)
180
- await renderOnce()
181
-
182
- const selection = text.getSelection()
183
- expect(selection).not.toBe(null)
184
- expect(text.getSelectedText()).toBe("Second")
185
- })
186
-
187
- it("should handle selection including multiple line breaks", async () => {
188
- const { text } = await createTextRenderable(currentRenderer, {
189
- content: "A\nB\nC\nD",
190
- selectable: true,
191
- })
192
-
193
- // Select from middle of first line to middle of last line
194
- await currentMouse.drag(text.x, text.y + 1, text.x + 1, text.y + 2)
195
- await renderOnce()
196
-
197
- const selection = text.getSelection()
198
- expect(selection).not.toBe(null)
199
- const selectedText = text.getSelectedText()
200
- expect(selectedText).toContain("\n")
201
- expect(selectedText).toContain("B")
202
- expect(selectedText).toContain("C")
203
- })
204
-
205
- it("should handle selection that includes line breaks at boundaries", async () => {
206
- const { text } = await createTextRenderable(currentRenderer, {
207
- content: "Line1\nLine2\nLine3",
208
- selectable: true,
209
- })
210
-
211
- // Select across line boundaries
212
- await currentMouse.drag(text.x + 4, text.y, text.x + 2, text.y + 1)
213
- await renderOnce()
214
-
215
- const selection = text.getSelection()
216
- expect(selection).not.toBe(null)
217
- const selectedText = text.getSelectedText()
218
- expect(selectedText).toContain("1")
219
- expect(selectedText).toContain("\n")
220
- expect(selectedText).toContain("Li")
221
- })
222
-
223
- it("should handle reverse selection (end before start)", async () => {
224
- const { text } = await createTextRenderable(currentRenderer, {
225
- content: "Hello World",
226
- selectable: true,
227
- })
228
-
229
- await currentMouse.drag(text.x + 11, text.y, text.x + 6, text.y)
230
- await renderOnce()
231
-
232
- const selection = text.getSelection()
233
- expect(selection).not.toBe(null)
234
- expect(selection!.start).toBe(6)
235
- expect(selection!.end).toBe(11)
236
-
237
- expect(text.getSelectedText()).toBe("World")
238
- })
239
- })
240
-
241
- describe("Selection Edge Cases", () => {
242
- it("should handle empty text", async () => {
243
- const { text } = await createTextRenderable(currentRenderer, {
244
- content: "",
245
- selectable: true,
246
- })
247
-
248
- await currentMouse.drag(text.x, text.y, text.x, text.y)
249
- await renderOnce()
250
-
251
- expect(text.hasSelection()).toBe(false)
252
- expect(text.getSelection()).toBe(null)
253
- expect(text.getSelectedText()).toBe("")
254
- })
255
-
256
- it("should handle single character selection", async () => {
257
- const { text } = await createTextRenderable(currentRenderer, {
258
- content: "A",
259
- selectable: true,
260
- })
261
-
262
- await currentMouse.drag(text.x, text.y, text.x + 1, text.y)
263
- await renderOnce()
264
-
265
- const selection = text.getSelection()
266
- expect(selection).not.toBe(null)
267
- expect(selection!.start).toBe(0)
268
- expect(selection!.end).toBe(1)
269
-
270
- expect(text.getSelectedText()).toBe("A")
271
- })
272
-
273
- it("should handle zero-width selection", async () => {
274
- const { text } = await createTextRenderable(currentRenderer, {
275
- content: "Hello World",
276
- selectable: true,
277
- })
278
-
279
- await currentMouse.drag(text.x + 5, text.y, text.x + 5, text.y)
280
- await renderOnce()
281
-
282
- expect(text.hasSelection()).toBe(false)
283
- expect(text.getSelection()).toBe(null)
284
- expect(text.getSelectedText()).toBe("")
285
- })
286
-
287
- it("should handle selection beyond text bounds", async () => {
288
- const { text } = await createTextRenderable(currentRenderer, {
289
- content: "Hi",
290
- selectable: true,
291
- })
292
-
293
- await currentMouse.drag(text.x, text.y, text.x + 10, text.y)
294
- await renderOnce()
295
-
296
- const selection = text.getSelection()
297
- expect(selection).not.toBe(null)
298
- expect(selection!.start).toBe(0)
299
- expect(selection!.end).toBe(2)
300
-
301
- expect(text.getSelectedText()).toBe("Hi")
302
- })
303
- })
304
-
305
- describe("Selection with Styled Text", () => {
306
- it("should handle styled text selection", async () => {
307
- const styledText = stringToStyledText("Hello World")
308
- styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text
309
-
310
- const { text } = await createTextRenderable(currentRenderer, {
311
- content: styledText,
312
- selectable: true,
313
- })
314
-
315
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
316
- await renderOnce()
317
-
318
- const selection = text.getSelection()
319
- expect(selection).not.toBe(null)
320
- expect(selection!.start).toBe(6)
321
- expect(selection!.end).toBe(11)
322
-
323
- expect(text.getSelectedText()).toBe("World")
324
- })
325
-
326
- it("should handle selection with different text colors", async () => {
327
- const { text } = await createTextRenderable(currentRenderer, {
328
- content: "Red and Blue",
329
- selectable: true,
330
- selectionBg: RGBA.fromValues(1, 1, 0, 1),
331
- selectionFg: RGBA.fromValues(0, 0, 0, 1),
332
- })
333
-
334
- await currentMouse.drag(text.x + 8, text.y, text.x + 12, text.y)
335
- await renderOnce()
336
-
337
- const selection = text.getSelection()
338
- expect(selection).not.toBe(null)
339
- expect(selection!.start).toBe(8)
340
- expect(selection!.end).toBe(12)
341
-
342
- expect(text.getSelectedText()).toBe("Blue")
343
- })
344
- })
345
-
346
- describe("Selection State Management", () => {
347
- it("should clear selection when selection is cleared", async () => {
348
- const { text } = await createTextRenderable(currentRenderer, {
349
- content: "Hello World",
350
- selectable: true,
351
- })
352
-
353
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
354
- await renderOnce()
355
- expect(text.hasSelection()).toBe(true)
356
-
357
- currentRenderer.clearSelection()
358
- await renderOnce()
359
-
360
- expect(text.hasSelection()).toBe(false)
361
- expect(text.getSelection()).toBe(null)
362
- expect(text.getSelectedText()).toBe("")
363
- })
364
-
365
- it("should handle multiple selection changes", async () => {
366
- const { text } = await createTextRenderable(currentRenderer, {
367
- content: "Hello World Test",
368
- selectable: true,
369
- })
370
-
371
- await currentMouse.drag(text.x + 0, text.y, text.x + 5, text.y)
372
- await renderOnce()
373
- expect(text.getSelectedText()).toBe("Hello")
374
- expect(text.getSelection()).toEqual({ start: 0, end: 5 })
375
-
376
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
377
- await renderOnce()
378
- expect(text.getSelectedText()).toBe("World")
379
- expect(text.getSelection()).toEqual({ start: 6, end: 11 })
380
-
381
- await currentMouse.drag(text.x + 12, text.y, text.x + 16, text.y)
382
- await renderOnce()
383
- expect(text.getSelectedText()).toBe("Test")
384
- expect(text.getSelection()).toEqual({ start: 12, end: 16 })
385
- })
386
- })
387
-
388
- describe("shouldStartSelection", () => {
389
- it("should return false for non-selectable text", async () => {
390
- const { text } = await createTextRenderable(currentRenderer, {
391
- content: "Hello World",
392
- selectable: false,
393
- })
394
-
395
- expect(text.shouldStartSelection(0, 0)).toBe(false)
396
- expect(text.shouldStartSelection(5, 0)).toBe(false)
397
- })
398
-
399
- it("should return true for selectable text within bounds", async () => {
400
- const { text } = await createTextRenderable(currentRenderer, {
401
- content: "Hello World",
402
- selectable: true,
403
- })
404
-
405
- expect(text.shouldStartSelection(0, 0)).toBe(true) // Start of text
406
- expect(text.shouldStartSelection(5, 0)).toBe(true) // Middle of text
407
- expect(text.shouldStartSelection(10, 0)).toBe(true) // End of text
408
- })
409
-
410
- it("should handle shouldStartSelection with multi-line text", async () => {
411
- const { text } = await createTextRenderable(currentRenderer, {
412
- content: "Line 1\nLine 2\nLine 3",
413
- selectable: true,
414
- })
415
-
416
- expect(text.shouldStartSelection(0, 0)).toBe(true) // Line 1 start
417
- expect(text.shouldStartSelection(2, 1)).toBe(true) // Line 2 middle
418
- expect(text.shouldStartSelection(5, 2)).toBe(true) // Line 3 end
419
- })
420
- })
421
-
422
- describe("Selection with Custom Dimensions", () => {
423
- it("should handle selection in constrained width", async () => {
424
- const { text } = await createTextRenderable(currentRenderer, {
425
- content: "This is a very long text that should wrap to multiple lines",
426
- width: 10,
427
- selectable: true,
428
- })
429
-
430
- await currentMouse.drag(text.x, text.y, text.x + 10, text.y + 2)
431
- await renderOnce()
432
-
433
- const selection = text.getSelection()
434
- expect(selection).not.toBe(null)
435
- expect(selection!.start).toBeGreaterThanOrEqual(0)
436
- expect(selection!.end).toBeGreaterThan(selection!.start)
437
- expect(text.getSelectedText().length).toBeGreaterThan(0)
438
- })
439
- })
440
-
441
- describe("Cross-Renderable Selection in Nested Boxes", () => {
442
- it("should handle selection across multiple nested text renderables in boxes", async () => {
443
- const { text: statusText } = await createTextRenderable(currentRenderer, {
444
- content: "Selected 5 chars:",
445
- selectable: true,
446
- fg: "#f0f6fc",
447
- top: 0,
448
- })
449
-
450
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
451
- content: '"Hello"',
452
- selectable: true,
453
- fg: "#7dd3fc",
454
- top: 1,
455
- })
456
-
457
- const { text: selectionMiddleText } = await createTextRenderable(currentRenderer, {
458
- content: "",
459
- selectable: true,
460
- fg: "#94a3b8",
461
- top: 2,
462
- })
463
-
464
- const { text: selectionEndText } = await createTextRenderable(currentRenderer, {
465
- content: "",
466
- selectable: true,
467
- fg: "#7dd3fc",
468
- top: 3,
469
- })
470
-
471
- const { text: debugText } = await createTextRenderable(currentRenderer, {
472
- content: "Selected renderables: 2/5",
473
- selectable: true,
474
- fg: "#e6edf3",
475
- top: 4,
476
- })
477
-
478
- // Simulate starting selection above the box and ending below/right of the box
479
- // This should cover all renderables in the "box"
480
- const allRenderables = [statusText, selectionStartText, selectionMiddleText, selectionEndText, debugText]
481
-
482
- await currentMouse.drag(0, 0, 50, 10)
483
- await renderOnce()
484
-
485
- expect(statusText.hasSelection()).toBe(true)
486
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
487
-
488
- expect(selectionStartText.hasSelection()).toBe(true)
489
- expect(selectionStartText.getSelectedText()).toBe('"Hello"')
490
-
491
- // Empty text renderables should not have selections since there's no content to select
492
- expect(selectionMiddleText.hasSelection()).toBe(false)
493
- expect(selectionMiddleText.getSelectedText()).toBe("")
494
-
495
- expect(selectionEndText.hasSelection()).toBe(false)
496
- expect(selectionEndText.getSelectedText()).toBe("")
497
-
498
- expect(debugText.hasSelection()).toBe(true)
499
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
500
-
501
- const globalSelectedText = currentRenderer.getSelection()?.getSelectedText()
502
-
503
- expect(globalSelectedText).toContain("Selected 5 chars:")
504
- expect(globalSelectedText).toContain('"Hello"')
505
- expect(globalSelectedText).toContain("Selected renderables: 2/5")
506
- })
507
-
508
- it("should automatically update selection when text content changes within covered area", async () => {
509
- const { text: statusText } = await createTextRenderable(currentRenderer, {
510
- content: "Selected 5 chars:",
511
- selectable: true,
512
- fg: "#f0f6fc",
513
- top: 0,
514
- wrapMode: "none",
515
- })
516
-
517
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
518
- top: 1,
519
- content: '"Hello"',
520
- selectable: true,
521
- fg: "#7dd3fc",
522
- wrapMode: "none",
523
- })
524
-
525
- const { text: debugText } = await createTextRenderable(currentRenderer, {
526
- top: 2,
527
- content: "Selected renderables: 2/5",
528
- selectable: true,
529
- fg: "#e6edf3",
530
- wrapMode: "none",
531
- })
532
-
533
- await currentMouse.drag(0, 0, 50, 5)
534
- await renderOnce()
535
-
536
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
537
- expect(selectionStartText.getSelectedText()).toBe('"Hello"')
538
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
539
-
540
- selectionStartText.content = '"Hello World Extended Selection"'
541
-
542
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
543
- expect(selectionStartText.getSelectedText()).toBe('"Hello World Extended Selection"')
544
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
545
-
546
- const updatedGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
547
-
548
- expect(updatedGlobalSelectedText).toContain('"Hello World Extended Selection"')
549
- expect(updatedGlobalSelectedText).toContain("Selected 5 chars:")
550
- expect(updatedGlobalSelectedText).toContain("Selected renderables: 2/5")
551
-
552
- debugText.content = "Selected renderables: 3/5 | Container: statusBox"
553
-
554
- expect(debugText.getSelectedText()).toBe("Selected renderables: 3/5 | Container: statusBox")
555
-
556
- const finalGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
557
-
558
- expect(finalGlobalSelectedText).toContain("Selected renderables: 3/5 | Container: statusBox")
559
- })
560
-
561
- it("should automatically update selection when text node content changes with clear and add", async () => {
562
- const { text: statusText } = await createTextRenderable(currentRenderer, {
563
- content: "",
564
- selectable: true,
565
- fg: "#f0f6fc",
566
- top: 0,
567
- wrapMode: "none",
568
- })
569
-
570
- const statusNode = new TextNodeRenderable({})
571
- statusNode.add("Selected 5 chars:")
572
- statusText.add(statusNode)
573
-
574
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
575
- content: "",
576
- selectable: true,
577
- fg: "#7dd3fc",
578
- top: 1,
579
- wrapMode: "none",
580
- })
581
-
582
- const selectionNode = new TextNodeRenderable({})
583
- selectionNode.add('"Hello"')
584
- selectionStartText.add(selectionNode)
585
-
586
- const { text: debugText } = await createTextRenderable(currentRenderer, {
587
- content: "",
588
- selectable: true,
589
- fg: "#e6edf3",
590
- top: 2,
591
- wrapMode: "none",
592
- })
593
-
594
- const debugNode = new TextNodeRenderable({})
595
- debugNode.add("Selected renderables: 2/5")
596
- debugText.add(debugNode)
597
-
598
- await currentMouse.drag(0, 0, 50, 5)
599
- await renderOnce()
600
-
601
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
602
- expect(selectionStartText.getSelectedText()).toBe('"Hello"')
603
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
604
-
605
- // Clear and add new content to the selection node
606
- selectionNode.clear()
607
- selectionNode.add('"Hello World Extended Selection"')
608
- await renderOnce()
609
-
610
- expect(statusText.getSelectedText()).toBe("Selected 5 chars:")
611
- expect(selectionStartText.getSelectedText()).toBe('"Hello World Extended Selection"')
612
- expect(debugText.getSelectedText()).toBe("Selected renderables: 2/5")
613
-
614
- const updatedGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
615
-
616
- expect(updatedGlobalSelectedText).toContain('"Hello World Extended Selection"')
617
- expect(updatedGlobalSelectedText).toContain("Selected 5 chars:")
618
- expect(updatedGlobalSelectedText).toContain("Selected renderables: 2/5")
619
-
620
- // Clear and add new content to the debug node
621
- debugNode.clear()
622
- debugNode.add("Selected renderables: 3/5 | Container: statusBox")
623
- await renderOnce()
624
-
625
- expect(debugText.getSelectedText()).toBe("Selected renderables: 3/5 | Container: statusBox")
626
-
627
- const finalGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()
628
-
629
- expect(finalGlobalSelectedText).toContain("Selected renderables: 3/5 | Container: statusBox")
630
- })
631
-
632
- it("should handle selection that starts above box and ends below/right of box", async () => {
633
- const { text: statusText } = await createTextRenderable(currentRenderer, {
634
- content: "Status: Selection active",
635
- selectable: true,
636
- fg: "#f0f6fc",
637
- top: 2,
638
- wrapMode: "none",
639
- })
640
-
641
- const { text: selectionStartText } = await createTextRenderable(currentRenderer, {
642
- content: "Start: (10,5)",
643
- selectable: true,
644
- fg: "#7dd3fc",
645
- top: 3,
646
- wrapMode: "none",
647
- })
648
-
649
- const { text: selectionEndText } = await createTextRenderable(currentRenderer, {
650
- content: "End: (45,12)",
651
- selectable: true,
652
- fg: "#7dd3fc",
653
- top: 4,
654
- wrapMode: "none",
655
- })
656
-
657
- const { text: debugText } = await createTextRenderable(currentRenderer, {
658
- content: "Debug: Cross-renderable selection spanning 3 elements",
659
- selectable: true,
660
- fg: "#e6edf3",
661
- top: 5,
662
- wrapMode: "none",
663
- })
664
-
665
- const allRenderables = [statusText, selectionStartText, selectionEndText, debugText]
666
-
667
- await currentMouse.drag(statusText.x, statusText.y, 60, 10)
668
- await renderOnce()
669
-
670
- allRenderables.forEach((renderable) => {
671
- expect(renderable.hasSelection()).toBe(true)
672
- })
673
-
674
- expect(statusText.getSelectedText()).toBe("Status: Selection active")
675
- expect(selectionStartText.getSelectedText()).toBe("Start: (10,5)")
676
- expect(selectionEndText.getSelectedText()).toBe("End: (45,12)")
677
- expect(debugText.getSelectedText()).toBe("Debug: Cross-renderable selection spanning 3 elements")
678
-
679
- const globalSelectedText = currentRenderer.getSelection()?.getSelectedText()
680
-
681
- expect(globalSelectedText).toContain("Status: Selection active")
682
- expect(globalSelectedText).toContain("Start: (10,5)")
683
- expect(globalSelectedText).toContain("End: (45,12)")
684
- expect(globalSelectedText).toContain("Debug: Cross-renderable selection spanning 3 elements")
685
- })
686
- })
687
-
688
- describe("TextNode Integration with getPlainText", () => {
689
- it("should render correct plain text after adding TextNodes", async () => {
690
- const { text, root } = await createTextRenderable(currentRenderer, {
691
- content: "",
692
- selectable: true,
693
- })
694
-
695
- const node1 = new TextNodeRenderable({
696
- fg: RGBA.fromValues(1, 0, 0, 1),
697
- bg: RGBA.fromValues(0, 0, 0, 1),
698
- })
699
- node1.add("Hello")
700
-
701
- const node2 = new TextNodeRenderable({
702
- fg: RGBA.fromValues(0, 1, 0, 1),
703
- bg: RGBA.fromValues(0, 0, 0, 1),
704
- })
705
- node2.add(" World")
706
-
707
- text.add(node1)
708
- text.add(node2)
709
-
710
- await renderOnce()
711
-
712
- expect(text.plainText).toBe("Hello World")
713
- })
714
-
715
- it("should render correct plain text after inserting TextNodes", async () => {
716
- const { text, root } = await createTextRenderable(currentRenderer, {
717
- content: "",
718
- selectable: true,
719
- })
720
-
721
- const node1 = new TextNodeRenderable({})
722
- node1.add("Hello")
723
-
724
- const node2 = new TextNodeRenderable({})
725
- node2.add(" World")
726
-
727
- const node3 = new TextNodeRenderable({})
728
- node3.add("!")
729
-
730
- text.add(node1)
731
- text.add(node2)
732
-
733
- text.insertBefore(node3, node2)
734
-
735
- await renderOnce()
736
-
737
- expect(text.plainText).toBe("Hello! World")
738
- })
739
-
740
- it("should render correct plain text after removing TextNodes", async () => {
741
- const { text, root } = await createTextRenderable(currentRenderer, {
742
- content: "",
743
- selectable: true,
744
- })
745
-
746
- const node1 = new TextNodeRenderable({})
747
- node1.add("Hello")
748
-
749
- const node2 = new TextNodeRenderable({})
750
- node2.add(" Cruel")
751
-
752
- const node3 = new TextNodeRenderable({})
753
- node3.add(" World")
754
-
755
- text.add(node1)
756
- text.add(node2)
757
- text.add(node3)
758
-
759
- await renderOnce()
760
- expect(text.plainText).toBe("Hello Cruel World")
761
-
762
- text.remove(node2.id)
763
-
764
- await renderOnce()
765
-
766
- expect(text.plainText).toBe("Hello World")
767
- })
768
-
769
- it("should handle simple add and remove operations", async () => {
770
- const { text, root } = await createTextRenderable(currentRenderer, {
771
- content: "",
772
- selectable: true,
773
- })
774
-
775
- const node = new TextNodeRenderable({})
776
- node.add("Test")
777
-
778
- text.add(node)
779
-
780
- await renderOnce()
781
- expect(text.plainText).toBe("Test")
782
-
783
- text.remove(node.id)
784
-
785
- await renderOnce()
786
- expect(text.plainText).toBe("")
787
- })
788
-
789
- it("should render correct plain text after clearing all TextNodes", async () => {
790
- const { text, root } = await createTextRenderable(currentRenderer, {
791
- content: "",
792
- selectable: true,
793
- })
794
-
795
- const node1 = new TextNodeRenderable({})
796
- node1.add("Hello")
797
-
798
- const node2 = new TextNodeRenderable({})
799
- node2.add(" World")
800
-
801
- text.add(node1)
802
- text.add(node2)
803
-
804
- await renderOnce()
805
- expect(text.plainText).toBe("Hello World")
806
-
807
- text.clear()
808
-
809
- await renderOnce()
810
-
811
- expect(text.plainText).toBe("")
812
- })
813
-
814
- it("should handle nested TextNode structures correctly", async () => {
815
- const { text, root } = await createTextRenderable(currentRenderer, {
816
- content: "",
817
- selectable: true,
818
- })
819
-
820
- // Create nested structure: Parent -> [Child1, Child2]
821
- const parent = new TextNodeRenderable({
822
- fg: RGBA.fromValues(1, 1, 0, 1),
823
- })
824
-
825
- const child1 = new TextNodeRenderable({
826
- fg: RGBA.fromValues(1, 0, 0, 1),
827
- })
828
- child1.add("Red")
829
-
830
- const child2 = new TextNodeRenderable({
831
- fg: RGBA.fromValues(0, 1, 0, 1),
832
- })
833
- child2.add(" Green")
834
-
835
- parent.add(child1)
836
- parent.add(child2)
837
-
838
- const standalone = new TextNodeRenderable({
839
- fg: RGBA.fromValues(0, 0, 1, 1),
840
- })
841
- standalone.add(" Blue")
842
-
843
- text.add(parent)
844
- text.add(standalone)
845
-
846
- await renderOnce()
847
-
848
- expect(text.plainText).toBe("Red Green Blue")
849
- })
850
-
851
- it("should handle mixed string and TextNode content", async () => {
852
- const { text, root } = await createTextRenderable(currentRenderer, {
853
- content: "",
854
- selectable: true,
855
- })
856
-
857
- const startNode = new TextNodeRenderable({})
858
- startNode.add("Start ")
859
-
860
- const node1 = new TextNodeRenderable({})
861
- node1.add("middle")
862
-
863
- const node2 = new TextNodeRenderable({})
864
- node2.add(" end")
865
-
866
- text.add(startNode)
867
- text.add(node1)
868
- text.add(node2)
869
-
870
- await renderOnce()
871
-
872
- expect(text.plainText).toBe("Start middle end")
873
- })
874
-
875
- it("should handle TextNode operations with inherited styles", async () => {
876
- const { text, root } = await createTextRenderable(currentRenderer, {
877
- content: "",
878
- selectable: true,
879
- fg: RGBA.fromValues(1, 1, 1, 1), // White default
880
- })
881
-
882
- const redParent = new TextNodeRenderable({
883
- fg: RGBA.fromValues(1, 0, 0, 1), // Red
884
- })
885
-
886
- const redChild = new TextNodeRenderable({})
887
-
888
- const greenGrandchild = new TextNodeRenderable({
889
- fg: RGBA.fromValues(0, 1, 0, 1), // Green
890
- })
891
- greenGrandchild.add("Green")
892
-
893
- redChild.add(greenGrandchild)
894
- redParent.add(redChild)
895
-
896
- const blueNode = new TextNodeRenderable({
897
- fg: RGBA.fromValues(0, 0, 1, 1), // Blue
898
- })
899
- blueNode.add(" Blue")
900
-
901
- text.add(redParent)
902
- text.add(blueNode)
903
-
904
- await renderOnce()
905
-
906
- expect(text.plainText).toBe("Green Blue")
907
- })
908
-
909
- it("should handle empty TextNodes correctly", async () => {
910
- const { text, root } = await createTextRenderable(currentRenderer, {
911
- content: "",
912
- selectable: true,
913
- })
914
-
915
- const emptyNode1 = new TextNodeRenderable({})
916
- const nodeWithText = new TextNodeRenderable({})
917
- nodeWithText.add("Text")
918
- const emptyNode2 = new TextNodeRenderable({})
919
-
920
- text.add(emptyNode1)
921
- text.add(nodeWithText)
922
- text.add(emptyNode2)
923
-
924
- await renderOnce()
925
-
926
- expect(text.plainText).toBe("Text")
927
- })
928
-
929
- it("should handle complex TextNode operations sequence", async () => {
930
- const { text, root } = await createTextRenderable(currentRenderer, {
931
- content: "",
932
- selectable: true,
933
- })
934
-
935
- const initialNode = new TextNodeRenderable({})
936
- initialNode.add("Initial")
937
-
938
- const nodeA = new TextNodeRenderable({})
939
- nodeA.add(" A")
940
-
941
- const nodeB = new TextNodeRenderable({})
942
- nodeB.add(" B")
943
-
944
- const nodeC = new TextNodeRenderable({})
945
- nodeC.add(" C")
946
-
947
- const nodeD = new TextNodeRenderable({})
948
- nodeD.add(" D")
949
-
950
- text.add(initialNode)
951
- text.add(nodeA)
952
- text.add(nodeB)
953
- text.add(nodeC)
954
- text.add(nodeD)
955
-
956
- await renderOnce()
957
- expect(text.plainText).toBe("Initial A B C D")
958
-
959
- text.remove(nodeB.id)
960
-
961
- await renderOnce()
962
- expect(text.plainText).toBe("Initial A C D")
963
-
964
- const nodeX = new TextNodeRenderable({})
965
- nodeX.add(" X")
966
- text.insertBefore(nodeX, nodeC)
967
-
968
- await renderOnce()
969
- expect(text.plainText).toBe("Initial A X C D")
970
-
971
- nodeX.add(" Y")
972
-
973
- await renderOnce()
974
- expect(text.plainText).toBe("Initial A X Y C D")
975
- })
976
-
977
- it("should inherit fg/bg colors from TextRenderable to TextNode children", async () => {
978
- const { text, root } = await createTextRenderable(currentRenderer, {
979
- content: "",
980
- selectable: true,
981
- fg: RGBA.fromValues(1, 0, 0, 1),
982
- bg: RGBA.fromValues(0, 0, 1, 1),
983
- })
984
-
985
- const child1 = new TextNodeRenderable({})
986
- child1.add("Child1")
987
-
988
- const child2 = new TextNodeRenderable({})
989
- child2.add(" Child2")
990
-
991
- text.add(child1)
992
- text.add(child2)
993
-
994
- await renderOnce()
995
-
996
- expect(text.plainText).toBe("Child1 Child2")
997
-
998
- const chunks = text.textNode.gatherWithInheritedStyle()
999
-
1000
- expect(chunks).toHaveLength(2)
1001
-
1002
- chunks.forEach((chunk) => {
1003
- expect(chunk.fg).toEqual(RGBA.fromValues(1, 0, 0, 1))
1004
- expect(chunk.bg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1005
- expect(chunk.attributes).toBe(0)
1006
- })
1007
-
1008
- expect(chunks[0].text).toBe("Child1")
1009
- expect(chunks[1].text).toBe(" Child2")
1010
- })
1011
-
1012
- it("should allow TextNode children to override parent TextRenderable colors", async () => {
1013
- const { text, root } = await createTextRenderable(currentRenderer, {
1014
- content: "",
1015
- selectable: true,
1016
- fg: RGBA.fromValues(1, 0, 0, 1),
1017
- bg: RGBA.fromValues(0, 0, 1, 1),
1018
- })
1019
-
1020
- const inheritingChild = new TextNodeRenderable({})
1021
- inheritingChild.add("Inherit")
1022
-
1023
- const overridingChild = new TextNodeRenderable({
1024
- fg: RGBA.fromValues(0, 1, 0, 1),
1025
- bg: RGBA.fromValues(1, 1, 0, 1),
1026
- })
1027
- overridingChild.add(" Override")
1028
-
1029
- const partialOverrideChild = new TextNodeRenderable({
1030
- fg: RGBA.fromValues(0, 0, 1, 1),
1031
- })
1032
- partialOverrideChild.add(" Partial")
1033
-
1034
- text.add(inheritingChild)
1035
- text.add(overridingChild)
1036
- text.add(partialOverrideChild)
1037
-
1038
- await renderOnce()
1039
-
1040
- expect(text.plainText).toBe("Inherit Override Partial")
1041
-
1042
- const chunks = text.textNode.gatherWithInheritedStyle()
1043
-
1044
- expect(chunks).toHaveLength(3)
1045
-
1046
- // First child: inherits both fg and bg from parent
1047
- expect(chunks[0].text).toBe("Inherit")
1048
- expect(chunks[0].fg).toEqual(RGBA.fromValues(1, 0, 0, 1))
1049
- expect(chunks[0].bg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1050
-
1051
- // Second child: overrides both fg and bg
1052
- expect(chunks[1].text).toBe(" Override")
1053
- expect(chunks[1].fg).toEqual(RGBA.fromValues(0, 1, 0, 1))
1054
- expect(chunks[1].bg).toEqual(RGBA.fromValues(1, 1, 0, 1))
1055
-
1056
- // Third child: overrides fg, inherits bg
1057
- expect(chunks[2].text).toBe(" Partial")
1058
- expect(chunks[2].fg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1059
- expect(chunks[2].bg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1060
- })
1061
-
1062
- it("should inherit TextRenderable colors through nested TextNode hierarchies", async () => {
1063
- const { text, root } = await createTextRenderable(currentRenderer, {
1064
- content: "",
1065
- selectable: true,
1066
- fg: RGBA.fromValues(0, 1, 0, 1),
1067
- bg: RGBA.fromValues(0, 0, 0, 1),
1068
- })
1069
-
1070
- const grandparent = new TextNodeRenderable({})
1071
- const parent = new TextNodeRenderable({})
1072
- const child = new TextNodeRenderable({})
1073
-
1074
- child.add("Deep")
1075
- parent.add("Nested ")
1076
- parent.add(child)
1077
- grandparent.add("Very ")
1078
- grandparent.add(parent)
1079
-
1080
- text.add(grandparent)
1081
-
1082
- await renderOnce()
1083
-
1084
- expect(text.plainText).toBe("Very Nested Deep")
1085
-
1086
- const chunks = text.textNode.gatherWithInheritedStyle()
1087
-
1088
- expect(chunks).toHaveLength(3)
1089
-
1090
- // All chunks should inherit the TextRenderable's green fg and black bg
1091
- chunks.forEach((chunk) => {
1092
- expect(chunk.fg).toEqual(RGBA.fromValues(0, 1, 0, 1))
1093
- expect(chunk.bg).toEqual(RGBA.fromValues(0, 0, 0, 1))
1094
- expect(chunk.attributes).toBe(0)
1095
- })
1096
-
1097
- expect(chunks[0].text).toBe("Very ")
1098
- expect(chunks[1].text).toBe("Nested ")
1099
- expect(chunks[2].text).toBe("Deep")
1100
- })
1101
-
1102
- it("should handle TextRenderable color changes affecting existing TextNode children", async () => {
1103
- const { text, root } = await createTextRenderable(currentRenderer, {
1104
- content: "",
1105
- selectable: true,
1106
- fg: RGBA.fromValues(1, 0, 0, 1),
1107
- bg: RGBA.fromValues(0, 0, 0, 1),
1108
- })
1109
-
1110
- const child1 = new TextNodeRenderable({})
1111
- child1.add("Before")
1112
-
1113
- const child2 = new TextNodeRenderable({})
1114
- child2.add(" Change")
1115
-
1116
- text.add(child1)
1117
- text.add(child2)
1118
-
1119
- await renderOnce()
1120
- expect(text.plainText).toBe("Before Change")
1121
-
1122
- text.fg = RGBA.fromValues(0, 0, 1, 1)
1123
- text.bg = RGBA.fromValues(1, 1, 1, 1)
1124
-
1125
- await renderOnce()
1126
-
1127
- const chunks = text.textNode.gatherWithInheritedStyle()
1128
-
1129
- expect(chunks).toHaveLength(2)
1130
-
1131
- chunks.forEach((chunk) => {
1132
- expect(chunk.fg).toEqual(RGBA.fromValues(0, 0, 1, 1))
1133
- expect(chunk.bg).toEqual(RGBA.fromValues(1, 1, 1, 1))
1134
- })
1135
-
1136
- expect(chunks[0].text).toBe("Before")
1137
- expect(chunks[1].text).toBe(" Change")
1138
- })
1139
-
1140
- it("should handle TextNode commands with multiple operations per render", async () => {
1141
- const { text, root } = await createTextRenderable(currentRenderer, {
1142
- content: "",
1143
- selectable: true,
1144
- })
1145
-
1146
- const node1 = new TextNodeRenderable({})
1147
- node1.add("First")
1148
-
1149
- const node2 = new TextNodeRenderable({})
1150
- node2.add("Second")
1151
-
1152
- const node3 = new TextNodeRenderable({})
1153
- node3.add("Third")
1154
-
1155
- text.add(node1)
1156
- text.add(node2)
1157
- text.insertBefore(node3, node1)
1158
-
1159
- node2.add(" Modified")
1160
-
1161
- await renderOnce()
1162
-
1163
- expect(text.plainText).toBe("ThirdFirstSecond Modified")
1164
- })
1165
- })
1166
-
1167
- describe("StyledText Integration", () => {
1168
- it("should render StyledText content correctly", async () => {
1169
- const styledText = stringToStyledText("Hello World")
1170
-
1171
- styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text
1172
- styledText.chunks[0].bg = RGBA.fromValues(0, 0, 0, 1) // Black background
1173
-
1174
- const { text } = await createTextRenderable(currentRenderer, {
1175
- content: styledText,
1176
- selectable: true,
1177
- })
1178
-
1179
- await renderOnce()
1180
-
1181
- expect(text.plainText).toBe("Hello World")
1182
- expect(text.width).toBeGreaterThan(0)
1183
- expect(text.height).toBeGreaterThan(0)
1184
- })
1185
-
1186
- it("should handle selection with StyledText content", async () => {
1187
- const styledText = stringToStyledText("Hello World")
1188
- styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text
1189
-
1190
- const { text } = await createTextRenderable(currentRenderer, {
1191
- content: styledText,
1192
- selectable: true,
1193
- })
1194
-
1195
- await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)
1196
- await renderOnce()
1197
-
1198
- const selection = text.getSelection()
1199
- expect(selection).not.toBe(null)
1200
- expect(selection!.start).toBe(6)
1201
- expect(selection!.end).toBe(11)
1202
- expect(text.getSelectedText()).toBe("World")
1203
- })
1204
-
1205
- it("should handle empty StyledText", async () => {
1206
- const emptyStyledText = stringToStyledText("")
1207
-
1208
- const { text, root } = await createTextRenderable(currentRenderer, {
1209
- content: emptyStyledText,
1210
- selectable: true,
1211
- })
1212
-
1213
- await renderOnce()
1214
-
1215
- expect(text.plainText).toBe("")
1216
- expect(text.hasSelection()).toBe(false)
1217
- expect(text.getSelectedText()).toBe("")
1218
- })
1219
-
1220
- it("should handle StyledText with multiple chunks", async () => {
1221
- const styledText = new StyledText([
1222
- { __isChunk: true, text: "Red", fg: RGBA.fromValues(1, 0, 0, 1), attributes: 1 },
1223
- { __isChunk: true, text: " ", fg: undefined, attributes: 0 },
1224
- { __isChunk: true, text: "Green", fg: RGBA.fromValues(0, 1, 0, 1), attributes: 2 },
1225
- { __isChunk: true, text: " ", fg: undefined, attributes: 0 },
1226
- { __isChunk: true, text: "Blue", fg: RGBA.fromValues(0, 0, 1, 1), attributes: 0 },
1227
- ])
1228
-
1229
- const { text } = await createTextRenderable(currentRenderer, {
1230
- content: styledText,
1231
- selectable: true,
1232
- })
1233
-
1234
- await renderOnce()
1235
-
1236
- expect(text.plainText).toBe("Red Green Blue")
1237
-
1238
- await currentMouse.drag(text.x + 4, text.y, text.x + 9, text.y)
1239
- await renderOnce()
1240
-
1241
- expect(text.getSelectedText()).toBe("Green")
1242
- })
1243
-
1244
- it("should handle StyledText with TextNodeRenderable children", async () => {
1245
- const { text } = await createTextRenderable(currentRenderer, {
1246
- content: "",
1247
- selectable: true,
1248
- })
1249
-
1250
- const baseNode = new TextNodeRenderable({})
1251
- baseNode.add("Base ")
1252
- text.add(baseNode)
1253
-
1254
- const styledNode = new TextNodeRenderable({
1255
- fg: RGBA.fromValues(1, 0, 0, 1),
1256
- })
1257
-
1258
- const nodeStyledText = new StyledText([
1259
- { __isChunk: true, text: "Styled", fg: RGBA.fromValues(0, 1, 0, 1), attributes: 1 },
1260
- ])
1261
-
1262
- styledNode.add(nodeStyledText)
1263
- text.add(styledNode)
1264
-
1265
- await renderOnce()
1266
-
1267
- expect(text.plainText).toBe("Base Styled")
1268
-
1269
- await currentMouse.drag(text.x + 5, text.y, text.x + 11, text.y)
1270
- await renderOnce()
1271
- expect(text.getSelectedText()).toBe("Styled")
1272
- })
1273
- })
1274
-
1275
- describe("Text Selection with Truncation", () => {
1276
- it("should not extend selection across ellipsis in single line", async () => {
1277
- const buffer = currentRenderer.currentRenderBuffer
1278
- const { text } = await createTextRenderable(currentRenderer, {
1279
- content: "0123456789ABCDEFGHIJ",
1280
- width: 10,
1281
- height: 1,
1282
- selectable: true,
1283
- selectionBg: RGBA.fromValues(1, 0, 0, 1),
1284
- truncate: true,
1285
- })
1286
-
1287
- await currentMouse.drag(text.x + 6, text.y, text.x + 3, text.y)
1288
- await renderOnce()
1289
-
1290
- expect(text.hasSelection()).toBe(true)
1291
-
1292
- const { bg } = buffer.buffers
1293
- const bufferWidth = buffer.width
1294
-
1295
- const ellipsisIdx = text.y * bufferWidth + text.x + 3
1296
- const ellipsisBgR = bg[ellipsisIdx * 4 + 0]
1297
- const ellipsisBgG = bg[ellipsisIdx * 4 + 1]
1298
- const ellipsisBgB = bg[ellipsisIdx * 4 + 2]
1299
-
1300
- expect(Math.abs(ellipsisBgR - 1.0)).toBeLessThan(0.05)
1301
- expect(Math.abs(ellipsisBgG - 0.0)).toBeLessThan(0.05)
1302
- expect(Math.abs(ellipsisBgB - 0.0)).toBeLessThan(0.05)
1303
- })
1304
-
1305
- it("should render selection end correctly across ellipsis in last line", async () => {
1306
- const buffer = currentRenderer.currentRenderBuffer
1307
- const { text } = await createTextRenderable(currentRenderer, {
1308
- content: "Line 1: This is a long line without wrapping\nLine 2: Another very long line that will be truncated",
1309
- width: 10,
1310
- height: 2,
1311
- selectable: true,
1312
- selectionBg: RGBA.fromValues(1, 0, 0, 1),
1313
- truncate: true,
1314
- wrapMode: "none",
1315
- })
1316
-
1317
- await currentMouse.drag(text.x + 6, text.y, text.x + 2, text.y + 1)
1318
- await renderOnce()
1319
-
1320
- expect(text.hasSelection()).toBe(true)
1321
-
1322
- const { bg } = buffer.buffers
1323
- const bufferWidth = buffer.width
1324
-
1325
- const ellipsisIdx = (text.y + 1) * bufferWidth + text.x + 3
1326
- const ellipsisBgR = bg[ellipsisIdx * 4 + 0]
1327
- const ellipsisBgG = bg[ellipsisIdx * 4 + 1]
1328
- const ellipsisBgB = bg[ellipsisIdx * 4 + 2]
1329
-
1330
- expect(Math.abs(ellipsisBgR - 1.0)).toBeGreaterThan(0.05)
1331
- expect(Math.abs(ellipsisBgG - 0.0)).toBeLessThan(0.05)
1332
- expect(Math.abs(ellipsisBgB - 0.0)).toBeLessThan(0.05)
1333
- })
1334
- })
1335
-
1336
- describe("Text Content Snapshots", () => {
1337
- it("should render basic text content correctly", async () => {
1338
- await createTextRenderable(currentRenderer, {
1339
- content: "Hello World",
1340
- left: 5,
1341
- top: 3,
1342
- })
1343
-
1344
- const frame = captureFrame()
1345
- expect(frame).toMatchSnapshot()
1346
- })
1347
-
1348
- it("should render multiline text content correctly", async () => {
1349
- await createTextRenderable(currentRenderer, {
1350
- content: "Line 1: Hello\nLine 2: World\nLine 3: Testing\nLine 4: Multiline",
1351
- left: 1,
1352
- top: 1,
1353
- })
1354
-
1355
- const frame = captureFrame()
1356
- expect(frame).toMatchSnapshot()
1357
- })
1358
-
1359
- it("should render text with graphemes/emojis correctly", async () => {
1360
- await createTextRenderable(currentRenderer, {
1361
- content: "Hello 🌍 World 👋\n Test 🚀 Emoji",
1362
- left: 0,
1363
- top: 2,
1364
- })
1365
-
1366
- const frame = captureFrame()
1367
- expect(frame).toMatchSnapshot()
1368
- })
1369
-
1370
- it("should render TextNode text composition correctly", async () => {
1371
- const { text } = await createTextRenderable(currentRenderer, {
1372
- content: "",
1373
- left: 0,
1374
- top: 0,
1375
- })
1376
-
1377
- const node1 = new TextNodeRenderable({})
1378
- node1.add("First")
1379
-
1380
- const node2 = new TextNodeRenderable({})
1381
- node2.add(" Second")
1382
-
1383
- const node3 = new TextNodeRenderable({})
1384
- node3.add(" Third")
1385
-
1386
- text.add(node1)
1387
- text.add(node2)
1388
- text.add(node3)
1389
-
1390
- await renderOnce()
1391
-
1392
- const frame = captureFrame()
1393
- expect(frame).toMatchSnapshot()
1394
- })
1395
-
1396
- it("should render text positioning correctly", async () => {
1397
- await createTextRenderable(currentRenderer, {
1398
- content: "Top",
1399
- position: "absolute",
1400
- left: 0,
1401
- top: 0,
1402
- })
1403
-
1404
- await createTextRenderable(currentRenderer, {
1405
- content: "Mid",
1406
- position: "absolute",
1407
- left: 8,
1408
- top: 2,
1409
- })
1410
-
1411
- await createTextRenderable(currentRenderer, {
1412
- content: "Bot",
1413
- position: "absolute",
1414
- left: 16,
1415
- top: 4,
1416
- })
1417
-
1418
- const frame = captureFrame()
1419
- expect(frame).toMatchSnapshot()
1420
- })
1421
-
1422
- it("should render empty buffer correctly", async () => {
1423
- currentRenderer.currentRenderBuffer.clear()
1424
- const frame = captureFrame()
1425
- expect(frame).toMatchSnapshot()
1426
- })
1427
-
1428
- it("should render text with character wrapping correctly", async () => {
1429
- const { text } = await createTextRenderable(currentRenderer, {
1430
- content: "This is a very long text that should wrap to multiple lines when wrap is enabled",
1431
- wrapMode: "char", // Explicitly test character wrapping
1432
- width: 15, // Force wrapping at 15 characters width
1433
- left: 0,
1434
- top: 0,
1435
- })
1436
-
1437
- const frame = captureFrame()
1438
- expect(frame).toMatchSnapshot()
1439
- })
1440
-
1441
- it("should render wrapped text with different content", async () => {
1442
- await createTextRenderable(currentRenderer, {
1443
- content: "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789",
1444
- wrapMode: "char", // Explicitly test character wrapping
1445
- width: 10, // Force wrapping at 10 characters width
1446
- left: 2,
1447
- top: 1,
1448
- })
1449
-
1450
- const frame = captureFrame()
1451
- expect(frame).toMatchSnapshot()
1452
- })
1453
-
1454
- it("should render wrapped text with emojis and graphemes", async () => {
1455
- await createTextRenderable(currentRenderer, {
1456
- content: "Hello 🌍 World 👋 This is a test with emojis 🚀 that should wrap properly",
1457
- wrapMode: "char", // Explicitly test character wrapping
1458
- width: 12, // Force wrapping at 12 characters width
1459
- left: 1,
1460
- top: 0,
1461
- })
1462
-
1463
- const frame = captureFrame()
1464
- expect(frame).toMatchSnapshot()
1465
- })
1466
-
1467
- it("should render wrapped multiline text correctly", async () => {
1468
- await createTextRenderable(currentRenderer, {
1469
- content: "First line with long content\nSecond line also with content\nThird line",
1470
- wrapMode: "char", // Explicitly test character wrapping
1471
- width: 8, // Force wrapping at 8 characters width
1472
- left: 0,
1473
- top: 1,
1474
- })
1475
-
1476
- const frame = captureFrame()
1477
- expect(frame).toMatchSnapshot()
1478
- })
1479
-
1480
- it("should render text with tab indicator correctly", async () => {
1481
- await createTextRenderable(currentRenderer, {
1482
- content: "Line 1\tTabbed\nLine 2\t\tDouble tab",
1483
- tabIndicator: "→",
1484
- tabIndicatorColor: RGBA.fromValues(0.5, 0.5, 0.5, 1),
1485
- left: 0,
1486
- top: 0,
1487
- })
1488
-
1489
- const frame = captureFrame()
1490
- expect(frame).toMatchSnapshot()
1491
- })
1492
-
1493
- it("should render word wrapped text with CJK and English correctly", async () => {
1494
- resize(60, 10)
1495
-
1496
- const { text } = await createTextRenderable(currentRenderer, {
1497
- content: "🌟 Unicode test: こんにちは世界 Hello World 你好世界",
1498
- wrapMode: "word",
1499
- width: 35,
1500
- left: 0,
1501
- top: 0,
1502
- })
1503
-
1504
- await renderOnce()
1505
-
1506
- const frame = captureFrame()
1507
- const lines = frame.split("\n").filter((l) => l.trim().length > 0)
1508
-
1509
- // Verify no character duplication - each character should appear only once
1510
- const line0 = lines[0] || ""
1511
- const line1 = lines[1] || ""
1512
-
1513
- const line0_ends_with_kai = line0.trimEnd().endsWith("界")
1514
- const line1_starts_with_kai = line1.trimStart().startsWith("界")
1515
-
1516
- // "界" should not appear on both lines (would indicate duplication bug)
1517
- expect(line0_ends_with_kai && line1_starts_with_kai).toBe(false)
1518
- })
1519
-
1520
- it("should not split English word 'Hello' in middle when word wrapping with CJK characters", async () => {
1521
- // This test reproduces the exact issue from text-truncation-demo.ts where "Hello"
1522
- // is incorrectly split as "Hell" on first line and "o World" on second line
1523
- // when word wrapping is enabled with CJK/emoji characters before it.
1524
- resize(60, 10)
1525
-
1526
- const { text } = await createTextRenderable(currentRenderer, {
1527
- content: "🌟 Unicode test: こんにちは世界 Hello World 你好世界 안녕하세요 🚀 More emoji: 🎨🎭🎪🎬🎮🎯",
1528
- wrapMode: "word",
1529
- width: 50, // Width that causes wrapping in the demo
1530
- left: 0,
1531
- top: 0,
1532
- })
1533
-
1534
- await renderOnce()
1535
-
1536
- const frame = captureFrame()
1537
-
1538
- const lines = frame.split("\n").filter((l) => l.trim().length > 0)
1539
-
1540
- // The word "Hello" should NOT be split in the middle
1541
- // Check for the specific incorrect split: "Hell" on one line, "o" starting the next
1542
- let foundIncorrectSplit = false
1543
- for (let i = 0; i < lines.length - 1; i++) {
1544
- const currentLine = lines[i] || ""
1545
- const nextLine = lines[i + 1] || ""
1546
-
1547
- // Check if current line ends with "Hell" (incorrect split)
1548
- if (currentLine.trimEnd().endsWith("Hell")) {
1549
- // And next line starts with "o" (the rest of "Hello")
1550
- if (nextLine.trimStart().startsWith("o")) {
1551
- foundIncorrectSplit = true
1552
- }
1553
- }
1554
- }
1555
-
1556
- // Verify "Hello" is not split as "Hell" + "o"
1557
- expect(foundIncorrectSplit).toBe(false)
1558
-
1559
- // Verify the word "Hello" appears complete on a single line
1560
- const fullText = lines.join(" ")
1561
- expect(fullText).toContain("Hello")
1562
-
1563
- // Verify "Hello" is not split in the middle
1564
- const helloLineIndex = lines.findIndex((line) => line.includes("Hello"))
1565
- expect(helloLineIndex).toBeGreaterThanOrEqual(0) // "Hello" should be found
1566
-
1567
- const helloLine = lines[helloLineIndex] || ""
1568
- // Verify "Hello" appears as a complete word on this line
1569
- expect(helloLine).toMatch(/Hello/)
1570
-
1571
- // Verify no previous line ends with "Hell" without "o"
1572
- if (helloLineIndex > 0) {
1573
- const prevLine = lines[helloLineIndex - 1] || ""
1574
- expect(prevLine.trimEnd().endsWith("Hell")).toBe(false)
1575
- }
1576
-
1577
- // Additional verification: "Hello World" should ideally be together
1578
- // (this is a nice-to-have, showing improved wrapping behavior)
1579
- expect(helloLine).toContain("Hello World")
1580
- })
1581
- })
1582
-
1583
- describe("Text Node Dimension Updates", () => {
1584
- it("should update dimensions and reposition subsequent elements when text nodes expand", async () => {
1585
- const { text: firstText } = await createTextRenderable(currentRenderer, {
1586
- content: "",
1587
- width: 20,
1588
- wrapMode: "char",
1589
- })
1590
-
1591
- const shortNode = new TextNodeRenderable({})
1592
- shortNode.add("Short")
1593
- firstText.add(shortNode)
1594
-
1595
- const { text: secondText } = await createTextRenderable(currentRenderer, {
1596
- content: "Second text",
1597
- })
1598
-
1599
- await renderOnce()
1600
- const initialFrame = captureFrame()
1601
- expect(initialFrame).toMatchSnapshot()
1602
-
1603
- expect(firstText.height).toEqual(1)
1604
- expect(secondText.y).toEqual(1)
1605
-
1606
- shortNode.add(" text that will definitely wrap")
1607
-
1608
- await renderOnce()
1609
-
1610
- const finalFrame = captureFrame()
1611
-
1612
- expect(firstText.height).toEqual(2)
1613
- expect(secondText.y).toEqual(2)
1614
-
1615
- expect(finalFrame).not.toBe(initialFrame)
1616
- expect(finalFrame).toMatchSnapshot()
1617
- })
1618
-
1619
- it("should handle multiple text node updates with complex layout changes", async () => {
1620
- resize(20, 10)
1621
- const { text: firstText } = await createTextRenderable(currentRenderer, {
1622
- width: 10,
1623
- wrapMode: "word",
1624
- })
1625
-
1626
- const node1 = TextNodeRenderable.fromString("First")
1627
- const node2 = TextNodeRenderable.fromString(" part")
1628
-
1629
- firstText.add(node1)
1630
- firstText.add(node2)
1631
-
1632
- const { text: secondText } = await createTextRenderable(currentRenderer, {
1633
- width: 12,
1634
- wrapMode: "word",
1635
- })
1636
- secondText.add("Middle text")
1637
-
1638
- const { text: thirdText } = await createTextRenderable(currentRenderer, {})
1639
- thirdText.add("Bottom text")
1640
-
1641
- await renderOnce()
1642
- const initialFrame = captureFrame()
1643
- expect(initialFrame).toMatchSnapshot()
1644
-
1645
- // Record initial positions
1646
- expect(firstText.height).toEqual(1)
1647
- expect(secondText.y).toEqual(1)
1648
- expect(thirdText.y).toEqual(2)
1649
-
1650
- node1.add(" of a sentence")
1651
- node2.add("that will wrap")
1652
-
1653
- await renderOnce()
1654
-
1655
- const finalFrame = captureFrame()
1656
- expect(finalFrame).toMatchSnapshot()
1657
-
1658
- expect(firstText.height).toEqual(5)
1659
- expect(secondText.y).toEqual(5)
1660
- expect(thirdText.y).toEqual(6)
1661
- })
1662
- })
1663
-
1664
- describe("Height and Width Measurement", () => {
1665
- it("should grow height for multiline text without wrapping", async () => {
1666
- const { text } = await createTextRenderable(currentRenderer, {
1667
- content: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
1668
- wrapMode: "none",
1669
- })
1670
-
1671
- await renderOnce()
1672
-
1673
- expect(text.height).toBe(5)
1674
- expect(text.width).toBeGreaterThanOrEqual(6)
1675
- })
1676
-
1677
- it("should grow height for wrapped text when wrapping enabled", async () => {
1678
- const { text } = await createTextRenderable(currentRenderer, {
1679
- content: "This is a very long line that will definitely wrap to multiple lines",
1680
- wrapMode: "word",
1681
- width: 15,
1682
- })
1683
-
1684
- await renderOnce()
1685
-
1686
- expect(text.height).toBeGreaterThan(1)
1687
- expect(text.width).toBeLessThanOrEqual(15)
1688
- })
1689
-
1690
- it("should measure full width when wrapping is disabled and not constrained by parent", async () => {
1691
- const longLine = "This is a very long line that would wrap but wrapping is disabled"
1692
- const { text } = await createTextRenderable(currentRenderer, {
1693
- content: longLine,
1694
- wrapMode: "none",
1695
- position: "absolute",
1696
- })
1697
-
1698
- await renderOnce()
1699
-
1700
- expect(text.height).toBe(1)
1701
- expect(text.width).toBe(longLine.length)
1702
- })
1703
-
1704
- it("should update height when content changes from single to multiline", async () => {
1705
- const { text } = await createTextRenderable(currentRenderer, {
1706
- content: "Single line",
1707
- wrapMode: "none",
1708
- })
1709
-
1710
- await renderOnce()
1711
- expect(text.height).toBe(1)
1712
-
1713
- text.content = "Line 1\nLine 2\nLine 3"
1714
- await renderOnce()
1715
-
1716
- expect(text.height).toBe(3)
1717
- })
1718
-
1719
- it("should update height when wrapping mode changes", async () => {
1720
- const { text } = await createTextRenderable(currentRenderer, {
1721
- content: "This is a long line that will wrap to multiple lines",
1722
- wrapMode: "none",
1723
- width: 15,
1724
- })
1725
-
1726
- await renderOnce()
1727
- const unwrappedHeight = text.height
1728
- expect(unwrappedHeight).toBe(1)
1729
- expect(text.width).toBe(15)
1730
-
1731
- text.wrapMode = "word"
1732
- await renderOnce()
1733
-
1734
- const wrappedHeight = text.height
1735
-
1736
- expect(wrappedHeight).toBeGreaterThan(unwrappedHeight)
1737
- expect(wrappedHeight).toBeGreaterThanOrEqual(3)
1738
- })
1739
-
1740
- it("should shrink height when content changes from multi-line to single line", async () => {
1741
- const { text } = await createTextRenderable(currentRenderer, {
1742
- content: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
1743
- wrapMode: "none",
1744
- })
1745
-
1746
- await renderOnce()
1747
- expect(text.height).toBe(5)
1748
-
1749
- text.content = "Single line"
1750
- await renderOnce()
1751
-
1752
- expect(text.height).toBe(1)
1753
- })
1754
-
1755
- it("should shrink width when replacing long line with shorter (wrapMode: none, position: absolute)", async () => {
1756
- const { text } = await createTextRenderable(currentRenderer, {
1757
- content: "This is a very long line with many characters",
1758
- wrapMode: "none",
1759
- position: "absolute",
1760
- })
1761
-
1762
- await renderOnce()
1763
- const initialWidth = text.width
1764
- expect(initialWidth).toBe(45) // length of the long line
1765
-
1766
- text.content = "Short"
1767
- await renderOnce()
1768
-
1769
- expect(text.width).toBe(5)
1770
- expect(text.width).toBeLessThan(initialWidth)
1771
- })
1772
- })
1773
-
1774
- describe("Width/Height Setter Layout Tests", () => {
1775
- it("should not shrink box when width is set via setter", async () => {
1776
- resize(40, 10)
1777
-
1778
- const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })
1779
- currentRenderer.root.add(container)
1780
-
1781
- const row = new BoxRenderable(currentRenderer, { flexDirection: "row", width: "100%" })
1782
- container.add(row)
1783
-
1784
- const indicator = new BoxRenderable(currentRenderer, { backgroundColor: "#f00" })
1785
- row.add(indicator)
1786
-
1787
- const indicatorText = new TextRenderable(currentRenderer, { content: ">" })
1788
- indicator.add(indicatorText)
1789
-
1790
- const content = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1791
- row.add(content)
1792
-
1793
- const contentText = new TextRenderable(currentRenderer, { content: "Content that takes up space" })
1794
- content.add(contentText)
1795
-
1796
- await renderOnce()
1797
-
1798
- const initialIndicatorWidth = indicator.width
1799
-
1800
- indicator.width = 5
1801
- await renderOnce()
1802
-
1803
- const frame = captureFrame()
1804
- expect(frame).toMatchSnapshot()
1805
-
1806
- expect(indicator.width).toBe(5)
1807
- expect(content.width).toBeGreaterThan(0)
1808
- expect(content.width).toBeLessThan(30) // Should be compressed but not zero
1809
- })
1810
-
1811
- it("should not shrink box when height is set via setter in column layout with text", async () => {
1812
- resize(30, 15)
1813
-
1814
- const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })
1815
- currentRenderer.root.add(outerBox)
1816
-
1817
- const column = new BoxRenderable(currentRenderer, { flexDirection: "column", height: "100%" })
1818
- outerBox.add(column)
1819
-
1820
- const header = new BoxRenderable(currentRenderer, { backgroundColor: "#f00" })
1821
- column.add(header)
1822
-
1823
- const headerText = new TextRenderable(currentRenderer, { content: "Header" })
1824
- header.add(headerText)
1825
-
1826
- const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1827
- column.add(mainContent)
1828
-
1829
- const mainText = new TextRenderable(currentRenderer, {
1830
- content: "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8",
1831
- })
1832
- mainContent.add(mainText)
1833
-
1834
- const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: "#00f" })
1835
- column.add(footer)
1836
-
1837
- const footerText = new TextRenderable(currentRenderer, { content: "Footer" })
1838
- footer.add(footerText)
1839
-
1840
- await renderOnce()
1841
-
1842
- header.height = 3
1843
- await renderOnce()
1844
-
1845
- const frame = captureFrame()
1846
- expect(frame).toMatchSnapshot()
1847
-
1848
- expect(header.height).toBe(3)
1849
- expect(mainContent.height).toBeGreaterThan(0)
1850
- expect(footer.height).toBe(2)
1851
- })
1852
-
1853
- it("should not shrink box when minWidth is set via setter", async () => {
1854
- resize(40, 10)
1855
-
1856
- const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })
1857
- currentRenderer.root.add(container)
1858
-
1859
- const row = new BoxRenderable(currentRenderer, { flexDirection: "row", width: "100%" })
1860
- container.add(row)
1861
-
1862
- const indicator = new BoxRenderable(currentRenderer, { backgroundColor: "#f00", flexShrink: 1 })
1863
- row.add(indicator)
1864
-
1865
- const indicatorText = new TextRenderable(currentRenderer, { content: ">" })
1866
- indicator.add(indicatorText)
1867
-
1868
- const content = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1869
- row.add(content)
1870
-
1871
- const contentText = new TextRenderable(currentRenderer, { content: "Content that takes up space" })
1872
- content.add(contentText)
1873
-
1874
- await renderOnce()
1875
-
1876
- indicator.minWidth = 5
1877
- await renderOnce()
1878
-
1879
- const frame = captureFrame()
1880
- expect(frame).toMatchSnapshot()
1881
-
1882
- expect(indicator.width).toBeGreaterThanOrEqual(5)
1883
- expect(content.width).toBeGreaterThan(0)
1884
- })
1885
-
1886
- it("should not shrink box when minHeight is set via setter in column layout with text", async () => {
1887
- resize(30, 15)
1888
-
1889
- const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })
1890
- currentRenderer.root.add(outerBox)
1891
-
1892
- const column = new BoxRenderable(currentRenderer, { flexDirection: "column", height: "100%" })
1893
- outerBox.add(column)
1894
-
1895
- const header = new BoxRenderable(currentRenderer, { backgroundColor: "#f00", flexShrink: 1 })
1896
- column.add(header)
1897
-
1898
- const headerText = new TextRenderable(currentRenderer, { content: "Header" })
1899
- header.add(headerText)
1900
-
1901
- const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1902
- column.add(mainContent)
1903
-
1904
- const mainText = new TextRenderable(currentRenderer, {
1905
- content: "Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7\nLine8",
1906
- })
1907
- mainContent.add(mainText)
1908
-
1909
- const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: "#00f" })
1910
- column.add(footer)
1911
-
1912
- const footerText = new TextRenderable(currentRenderer, { content: "Footer" })
1913
- footer.add(footerText)
1914
-
1915
- await renderOnce()
1916
-
1917
- header.minHeight = 3
1918
- await renderOnce()
1919
-
1920
- const frame = captureFrame()
1921
- expect(frame).toMatchSnapshot()
1922
-
1923
- expect(header.height).toBeGreaterThanOrEqual(3)
1924
- expect(mainContent.height).toBeGreaterThan(0)
1925
- expect(footer.height).toBe(2)
1926
- })
1927
-
1928
- it("should not shrink box when width is set from undefined via setter", async () => {
1929
- resize(40, 10)
1930
-
1931
- const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })
1932
- currentRenderer.root.add(container)
1933
-
1934
- const row = new BoxRenderable(currentRenderer, { flexDirection: "row", width: "100%" })
1935
- container.add(row)
1936
-
1937
- const indicator = new BoxRenderable(currentRenderer, { backgroundColor: "#f00", flexShrink: 1 })
1938
- row.add(indicator)
1939
-
1940
- const indicatorText = new TextRenderable(currentRenderer, { content: ">" })
1941
- indicator.add(indicatorText)
1942
-
1943
- const content = new BoxRenderable(currentRenderer, { backgroundColor: "#0f0", flexGrow: 1 })
1944
- row.add(content)
1945
-
1946
- const contentText = new TextRenderable(currentRenderer, { content: "Content that takes up space" })
1947
- content.add(contentText)
1948
-
1949
- await renderOnce()
1950
-
1951
- indicator.width = 5
1952
- await renderOnce()
1953
-
1954
- const frame = captureFrame()
1955
- expect(frame).toMatchSnapshot()
1956
-
1957
- expect(indicator.width).toBe(5)
1958
- expect(content.width).toBeGreaterThan(0)
1959
- })
1960
- })
1961
-
1962
- describe("Absolute Positioned Box with Text", () => {
1963
- it("should render text in absolute positioned box with padding and borders correctly", async () => {
1964
- resize(80, 20)
1965
-
1966
- const notificationBox = new BoxRenderable(currentRenderer, {
1967
- position: "absolute",
1968
- justifyContent: "center",
1969
- alignItems: "flex-start",
1970
- top: 2,
1971
- right: 2,
1972
- width: Math.min(60, 80 - 6),
1973
- paddingLeft: 2,
1974
- paddingRight: 2,
1975
- paddingTop: 1,
1976
- paddingBottom: 1,
1977
- backgroundColor: "#1e293b",
1978
- borderColor: "#3b82f6",
1979
- border: ["left", "right"],
1980
- })
1981
-
1982
- currentRenderer.root.add(notificationBox)
1983
-
1984
- // Wrap content in nested boxes with row layout and gap
1985
- const outerWrapperBox = new BoxRenderable(currentRenderer, {
1986
- flexDirection: "row",
1987
- paddingBottom: 1,
1988
- paddingTop: 1,
1989
- paddingLeft: 2,
1990
- paddingRight: 2,
1991
- gap: 2,
1992
- })
1993
- notificationBox.add(outerWrapperBox)
1994
-
1995
- const innerContentBox = new BoxRenderable(currentRenderer, {
1996
- flexGrow: 1,
1997
- gap: 1,
1998
- })
1999
- outerWrapperBox.add(innerContentBox)
2000
-
2001
- const titleText = new TextRenderable(currentRenderer, {
2002
- content: "Important Notification",
2003
- attributes: 1, // BOLD
2004
- marginBottom: 1,
2005
- fg: "#f8fafc",
2006
- })
2007
- innerContentBox.add(titleText)
2008
-
2009
- const messageText = new TextRenderable(currentRenderer, {
2010
- content:
2011
- "This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.",
2012
- fg: "#e2e8f0",
2013
- wrapMode: "word",
2014
- width: "100%",
2015
- })
2016
- innerContentBox.add(messageText)
2017
-
2018
- await renderOnce()
2019
-
2020
- const frame = captureFrame()
2021
- expect(frame).toMatchSnapshot()
2022
-
2023
- // Verify the box is positioned correctly
2024
- expect(notificationBox.x).toBeGreaterThan(0)
2025
- expect(notificationBox.y).toBe(2)
2026
- expect(notificationBox.width).toBe(60)
2027
-
2028
- // Note: With current Yoga behavior, nested flex boxes with width:"100%" inside
2029
- // an absolutely positioned parent with only maxWidth (no explicit width) causes
2030
- // the children to grow to their intrinsic size rather than being constrained
2031
- // This is Yoga's shrink-to-fit behavior with the circular dependency
2032
- // See: https://github.com/facebook/yoga/issues/1409
2033
- expect(outerWrapperBox.width).toBeGreaterThan(100)
2034
- expect(innerContentBox.width).toBeGreaterThan(100)
2035
- expect(messageText.width).toBeGreaterThan(100)
2036
- expect(messageText.height).toBe(1)
2037
- expect(messageText.plainText).toBe(
2038
- "This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.",
2039
- )
2040
- })
2041
-
2042
- it("should render text fully visible in absolute positioned box at various positions", async () => {
2043
- resize(100, 25)
2044
-
2045
- // Top-right positioned box
2046
- const topRightBox = new BoxRenderable(currentRenderer, {
2047
- position: "absolute",
2048
- top: 1,
2049
- right: 1,
2050
- maxWidth: 40,
2051
- paddingLeft: 1,
2052
- paddingRight: 1,
2053
- paddingTop: 0,
2054
- paddingBottom: 0,
2055
- backgroundColor: "#fef2f2",
2056
- borderColor: "#ef4444",
2057
- border: true,
2058
- })
2059
- currentRenderer.root.add(topRightBox)
2060
-
2061
- const topRightText = new TextRenderable(currentRenderer, {
2062
- content: "Error: File not found in the specified directory path",
2063
- fg: "#991b1b",
2064
- wrapMode: "word",
2065
- width: "100%",
2066
- })
2067
- topRightBox.add(topRightText)
2068
-
2069
- // Bottom-left positioned box
2070
- const bottomLeftBox = new BoxRenderable(currentRenderer, {
2071
- position: "absolute",
2072
- bottom: 1,
2073
- left: 1,
2074
- maxWidth: 35,
2075
- paddingLeft: 1,
2076
- paddingRight: 1,
2077
- backgroundColor: "#f0fdf4",
2078
- borderColor: "#22c55e",
2079
- border: ["top", "bottom"],
2080
- })
2081
- currentRenderer.root.add(bottomLeftBox)
2082
-
2083
- const bottomLeftText = new TextRenderable(currentRenderer, {
2084
- content: "Success: Operation completed successfully!",
2085
- fg: "#166534",
2086
- wrapMode: "word",
2087
- width: "100%",
2088
- })
2089
- bottomLeftBox.add(bottomLeftText)
2090
-
2091
- await renderOnce()
2092
-
2093
- const frame = captureFrame()
2094
- expect(frame).toMatchSnapshot()
2095
-
2096
- // Verify top-right box positioning and dimensions
2097
- expect(topRightBox.y).toBe(1)
2098
- expect(topRightBox.x).toBeGreaterThan(50)
2099
- expect(topRightBox.width).toBeGreaterThan(30)
2100
- expect(topRightBox.width).toBeLessThanOrEqual(40)
2101
-
2102
- // Verify top-right text renders with proper width
2103
- expect(topRightText.plainText).toBe("Error: File not found in the specified directory path")
2104
- expect(topRightText.width).toBeGreaterThan(25)
2105
- expect(topRightText.width).toBeLessThanOrEqual(38)
2106
- expect(topRightText.height).toBeGreaterThan(1)
2107
-
2108
- // Verify bottom-left box positioning and dimensions
2109
- expect(bottomLeftBox.x).toBe(1)
2110
- expect(bottomLeftBox.y).toBeGreaterThan(15)
2111
- expect(bottomLeftBox.width).toBeGreaterThan(25)
2112
- expect(bottomLeftBox.width).toBeLessThanOrEqual(35)
2113
-
2114
- // Verify bottom-left text renders with proper width
2115
- expect(bottomLeftText.plainText).toBe("Success: Operation completed successfully!")
2116
- expect(bottomLeftText.width).toBeGreaterThan(25)
2117
- expect(bottomLeftText.width).toBeLessThanOrEqual(33)
2118
- expect(bottomLeftText.height).toBeGreaterThan(1)
2119
- expect(bottomLeftText.width).toBeGreaterThan(0)
2120
- expect(bottomLeftText.width).toBeLessThanOrEqual(33) // maxWidth 35 - padding 2
2121
- })
2122
-
2123
- it("should handle width:100% text in absolute positioned box with constrained maxWidth", async () => {
2124
- resize(70, 15)
2125
-
2126
- const constrainedBox = new BoxRenderable(currentRenderer, {
2127
- position: "absolute",
2128
- top: 5,
2129
- left: 10,
2130
- maxWidth: 50,
2131
- paddingLeft: 3,
2132
- paddingRight: 3,
2133
- paddingTop: 2,
2134
- paddingBottom: 2,
2135
- backgroundColor: "#1e1e2e",
2136
- })
2137
- currentRenderer.root.add(constrainedBox)
2138
-
2139
- const longText = new TextRenderable(currentRenderer, {
2140
- content:
2141
- "This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.",
2142
- fg: "#cdd6f4",
2143
- wrapMode: "word",
2144
- width: "100%",
2145
- })
2146
- constrainedBox.add(longText)
2147
-
2148
- await renderOnce()
2149
-
2150
- const frame = captureFrame()
2151
- expect(frame).toMatchSnapshot()
2152
-
2153
- // Verify the box respects maxWidth
2154
- expect(constrainedBox.width).toBeLessThanOrEqual(50)
2155
- expect(constrainedBox.width).toBeGreaterThan(40)
2156
- expect(constrainedBox.x).toBe(10)
2157
- expect(constrainedBox.y).toBe(5)
2158
-
2159
- // Verify text wraps and fills available width
2160
- expect(longText.width).toBeGreaterThan(35)
2161
- expect(longText.width).toBeLessThanOrEqual(44)
2162
- expect(longText.height).toBeGreaterThanOrEqual(5)
2163
- expect(longText.plainText).toBe(
2164
- "This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.",
2165
- )
2166
- })
2167
-
2168
- it("should render multiple text elements in absolute positioned box with proper spacing", async () => {
2169
- resize(90, 20)
2170
-
2171
- const infoBox = new BoxRenderable(currentRenderer, {
2172
- position: "absolute",
2173
- justifyContent: "flex-start",
2174
- alignItems: "flex-start",
2175
- top: 3,
2176
- right: 5,
2177
- maxWidth: 45,
2178
- paddingLeft: 2,
2179
- paddingRight: 2,
2180
- paddingTop: 1,
2181
- paddingBottom: 1,
2182
- backgroundColor: "#eff6ff",
2183
- borderColor: "#3b82f6",
2184
- border: true,
2185
- })
2186
- currentRenderer.root.add(infoBox)
2187
-
2188
- const headerText = new TextRenderable(currentRenderer, {
2189
- content: "System Update",
2190
- attributes: 1, // BOLD
2191
- fg: "#1e40af",
2192
- })
2193
- infoBox.add(headerText)
2194
-
2195
- const bodyText = new TextRenderable(currentRenderer, {
2196
- content: "A new version is available with bug fixes and performance improvements.",
2197
- fg: "#1e3a8a",
2198
- wrapMode: "word",
2199
- width: "100%",
2200
- marginTop: 1,
2201
- })
2202
- infoBox.add(bodyText)
2203
-
2204
- const footerText = new TextRenderable(currentRenderer, {
2205
- content: "Click to install",
2206
- fg: "#60a5fa",
2207
- marginTop: 1,
2208
- })
2209
- infoBox.add(footerText)
2210
-
2211
- await renderOnce()
2212
-
2213
- const frame = captureFrame()
2214
- expect(frame).toMatchSnapshot()
2215
-
2216
- // Verify all texts are rendered with correct content
2217
- expect(headerText.plainText).toBe("System Update")
2218
- expect(bodyText.plainText).toBe("A new version is available with bug fixes and performance improvements.")
2219
- expect(footerText.plainText).toBe("Click to install")
2220
-
2221
- // Verify box dimensions are reasonable
2222
- expect(infoBox.width).toBeGreaterThan(35)
2223
- expect(infoBox.width).toBeLessThanOrEqual(45)
2224
-
2225
- // Verify header text renders properly
2226
- expect(headerText.width).toBeGreaterThan(10)
2227
- expect(headerText.height).toBe(1)
2228
-
2229
- // Verify body text fills width and wraps
2230
- expect(bodyText.width).toBeGreaterThan(30)
2231
- expect(bodyText.height).toBeGreaterThanOrEqual(2)
2232
-
2233
- // Verify footer text renders properly
2234
- expect(footerText.width).toBeGreaterThan(10)
2235
- expect(footerText.height).toBe(1)
2236
-
2237
- // Verify vertical spacing
2238
- expect(bodyText.y).toBeGreaterThan(headerText.y)
2239
- expect(footerText.y).toBeGreaterThan(bodyText.y)
2240
- })
2241
- })
2242
-
2243
- describe("Word Wrapping", () => {
2244
- it("should default to word wrap mode", async () => {
2245
- const { text } = await createTextRenderable(currentRenderer, {
2246
- content: "Hello World",
2247
- })
2248
-
2249
- expect(text.wrapMode).toBe("word")
2250
- })
2251
-
2252
- it("should wrap at word boundaries when using word mode", async () => {
2253
- await createTextRenderable(currentRenderer, {
2254
- content: "The quick brown fox jumps over the lazy dog",
2255
- wrapMode: "word",
2256
- width: 15,
2257
- left: 0,
2258
- top: 0,
2259
- })
2260
-
2261
- const frame = captureFrame()
2262
- expect(frame).toMatchSnapshot()
2263
- })
2264
-
2265
- it("should wrap at character boundaries when using char mode", async () => {
2266
- const { text } = await createTextRenderable(currentRenderer, {
2267
- content: "The quick brown fox jumps over the lazy dog",
2268
- wrapMode: "char",
2269
- width: 15,
2270
- left: 0,
2271
- top: 0,
2272
- })
2273
-
2274
- const frame = captureFrame()
2275
- expect(frame).toMatchSnapshot()
2276
- })
2277
-
2278
- it("should handle word wrapping with punctuation", async () => {
2279
- await createTextRenderable(currentRenderer, {
2280
- content: "Hello,World.Test-Example/Path",
2281
- wrapMode: "word",
2282
- width: 10,
2283
- left: 0,
2284
- top: 0,
2285
- })
2286
-
2287
- const frame = captureFrame()
2288
- expect(frame).toMatchSnapshot()
2289
- })
2290
-
2291
- it("should handle word wrapping with hyphens and dashes", async () => {
2292
- await createTextRenderable(currentRenderer, {
2293
- content: "self-contained multi-line text-wrapping example",
2294
- wrapMode: "word",
2295
- width: 12,
2296
- left: 0,
2297
- top: 0,
2298
- })
2299
-
2300
- const frame = captureFrame()
2301
- expect(frame).toMatchSnapshot()
2302
- })
2303
-
2304
- it("regression #651: should keep multi-byte UTF-8 words intact when wrapping in word mode", async () => {
2305
- resize(80, 24)
2306
-
2307
- await createTextRenderable(currentRenderer, {
2308
- content: "gyorskiszolgáló éttermek közül. Azóta alapjaiban értelmeztük újra a vendéglátást",
2309
- wrapMode: "word",
2310
- width: 40,
2311
- left: 0,
2312
- top: 0,
2313
- })
2314
-
2315
- const lines = captureFrame()
2316
- .split("\n")
2317
- .map((line) => line.trimEnd())
2318
- .filter((line) => line.length > 0)
2319
-
2320
- const expectedLines = ["gyorskiszolgáló éttermek közül. Azóta", "alapjaiban értelmeztük újra a", "vendéglátást"]
2321
-
2322
- expect(lines).toEqual(expectedLines)
2323
- })
2324
-
2325
- it("should dynamically change wrap mode", async () => {
2326
- const { text } = await createTextRenderable(currentRenderer, {
2327
- content: "The quick brown fox jumps",
2328
- wrapMode: "char",
2329
- width: 10,
2330
- left: 0,
2331
- top: 0,
2332
- })
2333
-
2334
- expect(text.wrapMode).toBe("char")
2335
-
2336
- // Change to word mode
2337
- text.wrapMode = "word"
2338
- await renderOnce()
2339
-
2340
- expect(text.wrapMode).toBe("word")
2341
- const frame = captureFrame()
2342
- expect(frame).toMatchSnapshot()
2343
- })
2344
-
2345
- it("should handle long words that exceed wrap width in word mode", async () => {
2346
- await createTextRenderable(currentRenderer, {
2347
- content: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
2348
- wrapMode: "word",
2349
- width: 10,
2350
- left: 0,
2351
- top: 0,
2352
- })
2353
-
2354
- // Since there's no word boundary, it should fall back to character wrapping
2355
- const frame = captureFrame()
2356
- expect(frame).toMatchSnapshot()
2357
- })
2358
-
2359
- it("should preserve empty lines with word wrapping", async () => {
2360
- await createTextRenderable(currentRenderer, {
2361
- content: "First line\n\nThird line",
2362
- wrapMode: "word",
2363
- width: 8,
2364
- left: 0,
2365
- top: 0,
2366
- })
2367
-
2368
- const frame = captureFrame()
2369
- expect(frame).toMatchSnapshot()
2370
- })
2371
-
2372
- it("should handle word wrapping with single character words", async () => {
2373
- await createTextRenderable(currentRenderer, {
2374
- content: "a b c d e f g h i j k l m n o p",
2375
- wrapMode: "word",
2376
- width: 8,
2377
- left: 0,
2378
- top: 0,
2379
- })
2380
-
2381
- const frame = captureFrame()
2382
- expect(frame).toMatchSnapshot()
2383
- })
2384
-
2385
- it("should compare char vs word wrapping with same content", async () => {
2386
- const content = "Hello wonderful world of text wrapping"
2387
-
2388
- // Test with char mode
2389
- const { text: charText } = await createTextRenderable(currentRenderer, {
2390
- content,
2391
- wrapMode: "char",
2392
- width: 12,
2393
- left: 0,
2394
- top: 0,
2395
- })
2396
-
2397
- const charFrame = captureFrame()
2398
-
2399
- // Remove the char text and add word text
2400
- currentRenderer.root.remove(charText.id)
2401
- await renderOnce()
2402
-
2403
- await createTextRenderable(currentRenderer, {
2404
- content,
2405
- wrapMode: "word",
2406
- width: 12,
2407
- left: 0,
2408
- top: 0,
2409
- })
2410
-
2411
- const wordFrame = captureFrame()
2412
-
2413
- // The frames should be different as word wrapping preserves word boundaries
2414
- expect(charFrame).not.toBe(wordFrame)
2415
- expect(wordFrame).toMatchSnapshot()
2416
- })
2417
-
2418
- it("should correctly wrap text when updating content via text.content", async () => {
2419
- const { text } = await createTextRenderable(currentRenderer, {
2420
- content: "Short text",
2421
- wrapMode: "word",
2422
- left: 0,
2423
- top: 0,
2424
- })
2425
-
2426
- await renderOnce()
2427
- const initialFrame = captureFrame()
2428
- expect(initialFrame).toMatchSnapshot()
2429
-
2430
- text.content = "This is a much longer text that should definitely wrap to multiple lines"
2431
-
2432
- await renderOnce()
2433
- const updatedFrame = captureFrame()
2434
- expect(updatedFrame).toMatchSnapshot()
2435
- })
2436
- })
2437
-
2438
- describe("Mouse Scrolling", () => {
2439
- it("should receive mouse scroll events", async () => {
2440
- resize(20, 10)
2441
-
2442
- const longText = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"
2443
- const { text } = await createTextRenderable(currentRenderer, {
2444
- content: longText,
2445
- wrapMode: "none",
2446
- })
2447
-
2448
- await renderOnce()
2449
-
2450
- let scrollEventReceived = false
2451
- let scrollInfo: any = null
2452
-
2453
- // Override the handler to capture events
2454
- const originalHandler = text.onMouseScroll
2455
- text.onMouseScroll = (event: any) => {
2456
- scrollEventReceived = true
2457
- scrollInfo = event.scroll
2458
- // Call original handler
2459
- if (originalHandler) {
2460
- originalHandler.call(text, event)
2461
- }
2462
- }
2463
-
2464
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2465
- await renderOnce()
2466
-
2467
- expect(scrollEventReceived).toBe(true)
2468
- expect(scrollInfo).toBeDefined()
2469
- expect(scrollInfo?.direction).toBe("down")
2470
- })
2471
-
2472
- it("should handle mouse scroll events for vertical scrolling", async () => {
2473
- resize(20, 5)
2474
-
2475
- const longText = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"
2476
- const { text } = await createTextRenderable(currentRenderer, {
2477
- content: longText,
2478
- wrapMode: "none",
2479
- })
2480
-
2481
- await renderOnce()
2482
-
2483
- // Initially should be at scroll position 0
2484
- expect(text.scrollY).toBe(0)
2485
- expect(text.scrollX).toBe(0)
2486
-
2487
- // Scroll down (each scroll event typically moves by 1)
2488
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2489
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2490
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2491
- await renderOnce()
2492
-
2493
- expect(text.scrollY).toBe(3)
2494
-
2495
- // Scroll up
2496
- await currentMouse.scroll(text.x + 1, text.y + 1, "up")
2497
- await renderOnce()
2498
-
2499
- expect(text.scrollY).toBe(2)
2500
- })
2501
-
2502
- it("should handle mouse scroll events for horizontal scrolling with unwrapped text", async () => {
2503
- resize(80, 5)
2504
-
2505
- const wideText =
2506
- "This is a very long line that extends way beyond the visible area and should definitely need scrolling"
2507
- const { text } = await createTextRenderable(currentRenderer, {
2508
- content: wideText,
2509
- wrapMode: "none",
2510
- width: 20,
2511
- maxWidth: 20,
2512
- })
2513
-
2514
- await renderOnce()
2515
-
2516
- expect(text.scrollX).toBe(0)
2517
- expect(text.scrollY).toBe(0)
2518
-
2519
- // Scroll right
2520
- for (let i = 0; i < 5; i++) {
2521
- await currentMouse.scroll(text.x + 1, text.y, "right")
2522
- }
2523
- await renderOnce()
2524
-
2525
- expect(text.scrollX).toBe(5)
2526
-
2527
- // Scroll left
2528
- await currentMouse.scroll(text.x + 1, text.y, "left")
2529
- await currentMouse.scroll(text.x + 1, text.y, "left")
2530
- await renderOnce()
2531
-
2532
- expect(text.scrollX).toBe(3)
2533
- })
2534
-
2535
- it("should not allow horizontal scrolling when text is wrapped", async () => {
2536
- resize(20, 5)
2537
-
2538
- const longText =
2539
- "Line 1 text\nLine 2 text\nLine 3 text\nLine 4 text\nLine 5 text\nLine 6 text\nLine 7 text\nLine 8 text"
2540
- const { text } = await createTextRenderable(currentRenderer, {
2541
- content: longText,
2542
- wrapMode: "word",
2543
- width: 15,
2544
- height: 3, // Constrain height to enable vertical scrolling
2545
- })
2546
-
2547
- await renderOnce()
2548
-
2549
- // Try to scroll horizontally
2550
- for (let i = 0; i < 5; i++) {
2551
- await currentMouse.scroll(text.x + 1, text.y + 1, "right")
2552
- }
2553
- await renderOnce()
2554
-
2555
- // Should not scroll horizontally when wrapped
2556
- expect(text.scrollX).toBe(0)
2557
-
2558
- // But vertical scrolling should still work if there's content
2559
- if (text.maxScrollY > 0) {
2560
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2561
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2562
- await renderOnce()
2563
-
2564
- expect(text.scrollY).toBe(2)
2565
- }
2566
- })
2567
-
2568
- it("should clamp scroll position to valid bounds", async () => {
2569
- resize(20, 5)
2570
-
2571
- const shortText = "Line 1\nLine 2\nLine 3"
2572
- const { text } = await createTextRenderable(currentRenderer, {
2573
- content: shortText,
2574
- wrapMode: "none",
2575
- })
2576
-
2577
- await renderOnce()
2578
-
2579
- // Try to scroll beyond content
2580
- for (let i = 0; i < 10; i++) {
2581
- await currentMouse.scroll(text.x + 1, text.y + 1, "down")
2582
- }
2583
- await renderOnce()
2584
-
2585
- // Should be clamped to maxScrollY
2586
- expect(text.scrollY).toBeLessThanOrEqual(text.maxScrollY)
2587
- expect(text.scrollY).toBeGreaterThanOrEqual(0)
2588
-
2589
- // Try to scroll up beyond 0
2590
- for (let i = 0; i < 20; i++) {
2591
- await currentMouse.scroll(text.x + 1, text.y + 1, "up")
2592
- }
2593
- await renderOnce()
2594
-
2595
- expect(text.scrollY).toBe(0)
2596
- })
2597
-
2598
- it("should expose scrollWidth and scrollHeight getters", async () => {
2599
- resize(20, 5)
2600
-
2601
- const text = "Line 1\nLine 2 with more content\nLine 3"
2602
- const { text: textRenderable } = await createTextRenderable(currentRenderer, {
2603
- content: text,
2604
- wrapMode: "none",
2605
- })
2606
-
2607
- await renderOnce()
2608
-
2609
- expect(textRenderable.scrollHeight).toBe(3) // 3 lines
2610
- expect(textRenderable.scrollWidth).toBeGreaterThan(0) // Max width of lines
2611
- })
2612
-
2613
- it("should calculate maxScrollY and maxScrollX correctly", async () => {
2614
- resize(20, 5)
2615
-
2616
- const text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8"
2617
- const { text: textRenderable } = await createTextRenderable(currentRenderer, {
2618
- content: text,
2619
- wrapMode: "none",
2620
- height: 5,
2621
- })
2622
-
2623
- await renderOnce()
2624
-
2625
- // maxScrollY should be scrollHeight - viewport height
2626
- expect(textRenderable.maxScrollY).toBe(Math.max(0, textRenderable.scrollHeight - textRenderable.height))
2627
-
2628
- // maxScrollX should be scrollWidth - viewport width
2629
- expect(textRenderable.maxScrollX).toBe(Math.max(0, textRenderable.scrollWidth - textRenderable.width))
2630
- })
2631
-
2632
- it("should update scroll position via setters", async () => {
2633
- resize(20, 5)
2634
-
2635
- const longText =
2636
- "Line 1 with some extra content\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"
2637
- const { text } = await createTextRenderable(currentRenderer, {
2638
- content: longText,
2639
- wrapMode: "none",
2640
- width: 20, // Constrain width
2641
- height: 5, // Constrain height
2642
- })
2643
-
2644
- await renderOnce()
2645
-
2646
- // Set scroll position directly
2647
- text.scrollY = 3
2648
- await renderOnce()
2649
-
2650
- expect(text.scrollY).toBe(3)
2651
-
2652
- // Set scrollX (only works if there's horizontal scrollable content)
2653
- if (text.maxScrollX > 0) {
2654
- text.scrollX = 2
2655
- await renderOnce()
2656
- expect(text.scrollX).toBe(2)
2657
- }
2658
- })
2659
- })
2660
- })