@fairyhunter13/opentui-core 0.1.113 → 0.1.114
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.
- package/dev/keypress-debug-renderer.ts +148 -0
- package/dev/keypress-debug.ts +43 -0
- package/dev/print-env-vars.ts +32 -0
- package/dev/test-tmux-graphics-334.sh +68 -0
- package/dev/thai-debug-test.ts +68 -0
- package/docs/development.md +144 -0
- package/package.json +62 -53
- package/scripts/build.ts +400 -0
- package/scripts/publish.ts +60 -0
- package/src/3d/SpriteResourceManager.ts +286 -0
- package/src/3d/SpriteUtils.ts +70 -0
- package/src/3d/TextureUtils.ts +196 -0
- package/src/3d/ThreeRenderable.ts +197 -0
- package/src/3d/WGPURenderer.ts +294 -0
- package/src/3d/animation/ExplodingSpriteEffect.ts +513 -0
- package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +429 -0
- package/src/3d/animation/SpriteAnimator.ts +633 -0
- package/src/3d/animation/SpriteParticleGenerator.ts +435 -0
- package/src/3d/canvas.ts +464 -0
- package/src/3d/index.ts +12 -0
- package/src/3d/physics/PlanckPhysicsAdapter.ts +72 -0
- package/src/3d/physics/RapierPhysicsAdapter.ts +66 -0
- package/src/3d/physics/physics-interface.ts +31 -0
- package/src/3d/shaders/supersampling.wgsl +201 -0
- package/src/3d.ts +3 -0
- package/src/NativeSpanFeed.ts +300 -0
- package/src/Renderable.ts +1704 -0
- package/src/__snapshots__/buffer.test.ts.snap +28 -0
- package/src/animation/Timeline.test.ts +2709 -0
- package/src/animation/Timeline.ts +598 -0
- package/src/ansi.ts +18 -0
- package/src/benchmark/attenuation-benchmark.ts +81 -0
- package/src/benchmark/colormatrix-benchmark.ts +128 -0
- package/src/benchmark/gain-benchmark.ts +80 -0
- package/src/benchmark/latest-all-bench-run.json +707 -0
- package/src/benchmark/latest-async-bench-run.json +336 -0
- package/src/benchmark/latest-default-bench-run.json +657 -0
- package/src/benchmark/latest-large-bench-run.json +707 -0
- package/src/benchmark/latest-quick-bench-run.json +207 -0
- package/src/benchmark/markdown-benchmark.ts +1796 -0
- package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
- package/src/benchmark/native-span-feed-benchmark.md +56 -0
- package/src/benchmark/native-span-feed-benchmark.ts +596 -0
- package/src/benchmark/native-span-feed-compare.ts +280 -0
- package/src/benchmark/renderer-benchmark.ts +754 -0
- package/src/benchmark/text-table-benchmark.ts +948 -0
- package/src/buffer.test.ts +291 -0
- package/src/buffer.ts +554 -0
- package/src/console.test.ts +612 -0
- package/src/console.ts +1254 -0
- package/src/edit-buffer.test.ts +1769 -0
- package/src/edit-buffer.ts +411 -0
- package/src/editor-view.test.ts +1032 -0
- package/src/editor-view.ts +284 -0
- package/src/examples/ascii-font-selection-demo.ts +245 -0
- package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
- package/src/examples/assets/concrete.png +0 -0
- package/src/examples/assets/crate.png +0 -0
- package/src/examples/assets/crate_emissive.png +0 -0
- package/src/examples/assets/forrest_background.png +0 -0
- package/src/examples/assets/hast-example.json +1018 -0
- package/src/examples/assets/heart.png +0 -0
- package/src/examples/assets/main_char_heavy_attack.png +0 -0
- package/src/examples/assets/main_char_idle.png +0 -0
- package/src/examples/assets/main_char_jump_end.png +0 -0
- package/src/examples/assets/main_char_jump_landing.png +0 -0
- package/src/examples/assets/main_char_jump_start.png +0 -0
- package/src/examples/assets/main_char_run_loop.png +0 -0
- package/src/examples/assets/roughness_map.jpg +0 -0
- package/src/examples/build.ts +115 -0
- package/src/examples/code-demo.ts +924 -0
- package/src/examples/console-demo.ts +358 -0
- package/src/examples/core-plugin-slots-demo.ts +759 -0
- package/src/examples/diff-demo.ts +701 -0
- package/src/examples/draggable-three-demo.ts +259 -0
- package/src/examples/editor-demo.ts +322 -0
- package/src/examples/extmarks-demo.ts +196 -0
- package/src/examples/focus-restore-demo.ts +310 -0
- package/src/examples/fonts.ts +245 -0
- package/src/examples/fractal-shader-demo.ts +268 -0
- package/src/examples/framebuffer-demo.ts +674 -0
- package/src/examples/full-unicode-demo.ts +241 -0
- package/src/examples/golden-star-demo.ts +933 -0
- package/src/examples/grayscale-buffer-demo.ts +249 -0
- package/src/examples/hast-syntax-highlighting-demo.ts +129 -0
- package/src/examples/index.ts +926 -0
- package/src/examples/input-demo.ts +377 -0
- package/src/examples/input-select-layout-demo.ts +425 -0
- package/src/examples/install.sh +143 -0
- package/src/examples/keypress-debug-demo.ts +452 -0
- package/src/examples/lib/HexList.ts +122 -0
- package/src/examples/lib/PaletteGrid.ts +125 -0
- package/src/examples/lib/standalone-keys.ts +25 -0
- package/src/examples/lib/tab-controller.ts +243 -0
- package/src/examples/lights-phong-demo.ts +290 -0
- package/src/examples/link-demo.ts +220 -0
- package/src/examples/live-state-demo.ts +480 -0
- package/src/examples/markdown-demo.ts +725 -0
- package/src/examples/mouse-interaction-demo.ts +428 -0
- package/src/examples/nested-zindex-demo.ts +357 -0
- package/src/examples/opacity-example.ts +235 -0
- package/src/examples/opentui-demo.ts +1057 -0
- package/src/examples/physx-planck-2d-demo.ts +623 -0
- package/src/examples/physx-rapier-2d-demo.ts +655 -0
- package/src/examples/relative-positioning-demo.ts +323 -0
- package/src/examples/scroll-example.ts +214 -0
- package/src/examples/scrollbox-mouse-test.ts +112 -0
- package/src/examples/scrollbox-overlay-hit-test.ts +206 -0
- package/src/examples/select-demo.ts +237 -0
- package/src/examples/shader-cube-demo.ts +1015 -0
- package/src/examples/simple-layout-example.ts +591 -0
- package/src/examples/slider-demo.ts +617 -0
- package/src/examples/split-mode-demo.ts +453 -0
- package/src/examples/sprite-animation-demo.ts +443 -0
- package/src/examples/sprite-particle-generator-demo.ts +486 -0
- package/src/examples/static-sprite-demo.ts +193 -0
- package/src/examples/sticky-scroll-example.ts +308 -0
- package/src/examples/styled-text-demo.ts +282 -0
- package/src/examples/tab-select-demo.ts +219 -0
- package/src/examples/terminal-title.ts +29 -0
- package/src/examples/terminal.ts +305 -0
- package/src/examples/text-node-demo.ts +416 -0
- package/src/examples/text-selection-demo.ts +377 -0
- package/src/examples/text-table-demo.ts +503 -0
- package/src/examples/text-truncation-demo.ts +481 -0
- package/src/examples/text-wrap.ts +757 -0
- package/src/examples/texture-loading-demo.ts +259 -0
- package/src/examples/timeline-example.ts +670 -0
- package/src/examples/transparency-demo.ts +400 -0
- package/src/examples/vnode-composition-demo.ts +404 -0
- package/src/examples/wide-grapheme-overlay-demo.ts +280 -0
- package/src/index.ts +24 -0
- package/src/lib/KeyHandler.integration.test.ts +292 -0
- package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
- package/src/lib/KeyHandler.test.ts +662 -0
- package/src/lib/KeyHandler.ts +222 -0
- package/src/lib/RGBA.test.ts +984 -0
- package/src/lib/RGBA.ts +204 -0
- package/src/lib/ascii.font.ts +330 -0
- package/src/lib/border.test.ts +83 -0
- package/src/lib/border.ts +170 -0
- package/src/lib/bunfs.test.ts +27 -0
- package/src/lib/bunfs.ts +18 -0
- package/src/lib/clipboard.test.ts +41 -0
- package/src/lib/clipboard.ts +47 -0
- package/src/lib/clock.ts +35 -0
- package/src/lib/data-paths.test.ts +133 -0
- package/src/lib/data-paths.ts +109 -0
- package/src/lib/debounce.ts +106 -0
- package/src/lib/detect-links.test.ts +98 -0
- package/src/lib/detect-links.ts +56 -0
- package/src/lib/env.test.ts +228 -0
- package/src/lib/env.ts +209 -0
- package/src/lib/extmarks-history.ts +51 -0
- package/src/lib/extmarks-multiwidth.test.ts +322 -0
- package/src/lib/extmarks.test.ts +3457 -0
- package/src/lib/extmarks.ts +843 -0
- package/src/lib/fonts/block.json +405 -0
- package/src/lib/fonts/grid.json +265 -0
- package/src/lib/fonts/huge.json +741 -0
- package/src/lib/fonts/pallet.json +314 -0
- package/src/lib/fonts/shade.json +591 -0
- package/src/lib/fonts/slick.json +321 -0
- package/src/lib/fonts/tiny.json +69 -0
- package/src/lib/hast-styled-text.ts +59 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/keymapping.test.ts +317 -0
- package/src/lib/keymapping.ts +115 -0
- package/src/lib/objects-in-viewport.test.ts +787 -0
- package/src/lib/objects-in-viewport.ts +153 -0
- package/src/lib/output.capture.ts +58 -0
- package/src/lib/parse.keypress-kitty.protocol.test.ts +340 -0
- package/src/lib/parse.keypress-kitty.test.ts +663 -0
- package/src/lib/parse.keypress-kitty.ts +439 -0
- package/src/lib/parse.keypress.test.ts +1849 -0
- package/src/lib/parse.keypress.ts +397 -0
- package/src/lib/parse.mouse.test.ts +552 -0
- package/src/lib/parse.mouse.ts +232 -0
- package/src/lib/paste.ts +16 -0
- package/src/lib/queue.ts +65 -0
- package/src/lib/renderable.validations.test.ts +87 -0
- package/src/lib/renderable.validations.ts +83 -0
- package/src/lib/scroll-acceleration.ts +98 -0
- package/src/lib/selection.ts +240 -0
- package/src/lib/singleton.ts +28 -0
- package/src/lib/stdin-parser.test.ts +2290 -0
- package/src/lib/stdin-parser.ts +1810 -0
- package/src/lib/styled-text.ts +178 -0
- package/src/lib/terminal-capability-detection.test.ts +202 -0
- package/src/lib/terminal-capability-detection.ts +79 -0
- package/src/lib/terminal-palette.test.ts +878 -0
- package/src/lib/terminal-palette.ts +383 -0
- package/src/lib/tree-sitter/assets/README.md +118 -0
- package/src/lib/tree-sitter/assets/update.ts +334 -0
- package/src/lib/tree-sitter/assets.d.ts +9 -0
- package/src/lib/tree-sitter/cache.test.ts +273 -0
- package/src/lib/tree-sitter/client.test.ts +1165 -0
- package/src/lib/tree-sitter/client.ts +607 -0
- package/src/lib/tree-sitter/default-parsers.ts +86 -0
- package/src/lib/tree-sitter/download-utils.ts +148 -0
- package/src/lib/tree-sitter/index.ts +28 -0
- package/src/lib/tree-sitter/parser.worker.ts +1042 -0
- package/src/lib/tree-sitter/parsers-config.ts +81 -0
- package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
- package/src/lib/tree-sitter/resolve-ft.ts +189 -0
- package/src/lib/tree-sitter/types.ts +82 -0
- package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
- package/src/lib/tree-sitter-styled-text.ts +306 -0
- package/src/lib/validate-dir-name.ts +55 -0
- package/src/lib/yoga.options.test.ts +628 -0
- package/src/lib/yoga.options.ts +346 -0
- package/src/plugins/core-slot.ts +579 -0
- package/src/plugins/registry.ts +402 -0
- package/src/plugins/types.ts +46 -0
- package/src/post/effects.ts +930 -0
- package/src/post/filters.ts +489 -0
- package/src/post/matrices.ts +288 -0
- package/src/renderables/ASCIIFont.ts +219 -0
- package/src/renderables/Box.test.ts +205 -0
- package/src/renderables/Box.ts +326 -0
- package/src/renderables/Code.test.ts +2062 -0
- package/src/renderables/Code.ts +357 -0
- package/src/renderables/Diff.regression.test.ts +226 -0
- package/src/renderables/Diff.test.ts +3101 -0
- package/src/renderables/Diff.ts +1211 -0
- package/src/renderables/EditBufferRenderable.test.ts +288 -0
- package/src/renderables/EditBufferRenderable.ts +1166 -0
- package/src/renderables/FrameBuffer.ts +47 -0
- package/src/renderables/Input.test.ts +1228 -0
- package/src/renderables/Input.ts +247 -0
- package/src/renderables/LineNumberRenderable.ts +724 -0
- package/src/renderables/Markdown.ts +1393 -0
- package/src/renderables/ScrollBar.ts +422 -0
- package/src/renderables/ScrollBox.ts +883 -0
- package/src/renderables/Select.test.ts +1033 -0
- package/src/renderables/Select.ts +524 -0
- package/src/renderables/Slider.test.ts +456 -0
- package/src/renderables/Slider.ts +342 -0
- package/src/renderables/TabSelect.test.ts +197 -0
- package/src/renderables/TabSelect.ts +455 -0
- package/src/renderables/Text.selection-buffer.test.ts +123 -0
- package/src/renderables/Text.test.ts +2660 -0
- package/src/renderables/Text.ts +147 -0
- package/src/renderables/TextBufferRenderable.ts +518 -0
- package/src/renderables/TextNode.test.ts +1058 -0
- package/src/renderables/TextNode.ts +325 -0
- package/src/renderables/TextTable.test.ts +1421 -0
- package/src/renderables/TextTable.ts +1344 -0
- package/src/renderables/Textarea.ts +430 -0
- package/src/renderables/TimeToFirstDraw.ts +89 -0
- package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
- package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
- package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
- package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +816 -0
- package/src/renderables/__tests__/LineNumberRenderable.test.ts +1865 -0
- package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
- package/src/renderables/__tests__/Markdown.code-colors.test.ts +242 -0
- package/src/renderables/__tests__/Markdown.test.ts +2518 -0
- package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
- package/src/renderables/__tests__/Textarea.buffer.test.ts +682 -0
- package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
- package/src/renderables/__tests__/Textarea.editing.test.ts +2041 -0
- package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
- package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
- package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
- package/src/renderables/__tests__/Textarea.keybinding.test.ts +3149 -0
- package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
- package/src/renderables/__tests__/Textarea.rendering.test.ts +1866 -0
- package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
- package/src/renderables/__tests__/Textarea.selection.test.ts +1590 -0
- package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
- package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
- package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
- package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
- package/src/renderables/__tests__/markdown-parser.test.ts +217 -0
- package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
- package/src/renderables/composition/README.md +8 -0
- package/src/renderables/composition/VRenderable.ts +32 -0
- package/src/renderables/composition/constructs.ts +127 -0
- package/src/renderables/composition/vnode.ts +289 -0
- package/src/renderables/index.ts +23 -0
- package/src/renderables/markdown-parser.ts +66 -0
- package/src/renderer.ts +2681 -0
- package/src/runtime-plugin-support.ts +39 -0
- package/src/runtime-plugin.ts +615 -0
- package/src/syntax-style.test.ts +841 -0
- package/src/syntax-style.ts +257 -0
- package/src/testing/README.md +210 -0
- package/src/testing/capture-spans.test.ts +194 -0
- package/src/testing/integration.test.ts +276 -0
- package/src/testing/manual-clock.ts +117 -0
- package/src/testing/mock-keys.test.ts +1378 -0
- package/src/testing/mock-keys.ts +457 -0
- package/src/testing/mock-mouse.test.ts +218 -0
- package/src/testing/mock-mouse.ts +247 -0
- package/src/testing/mock-tree-sitter-client.ts +73 -0
- package/src/testing/spy.ts +13 -0
- package/src/testing/test-recorder.test.ts +415 -0
- package/src/testing/test-recorder.ts +145 -0
- package/src/testing/test-renderer.ts +132 -0
- package/src/testing.ts +7 -0
- package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
- package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
- package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
- package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
- package/src/tests/allocator-stats.test.ts +38 -0
- package/src/tests/destroy-during-render.test.ts +200 -0
- package/src/tests/destroy-on-exit.fixture.ts +36 -0
- package/src/tests/destroy-on-exit.test.ts +41 -0
- package/src/tests/hover-cursor.test.ts +98 -0
- package/src/tests/native-span-feed-async.test.ts +173 -0
- package/src/tests/native-span-feed-close.test.ts +120 -0
- package/src/tests/native-span-feed-coverage.test.ts +227 -0
- package/src/tests/native-span-feed-edge-cases.test.ts +352 -0
- package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
- package/src/tests/opacity.test.ts +123 -0
- package/src/tests/renderable.snapshot.test.ts +524 -0
- package/src/tests/renderable.test.ts +1281 -0
- package/src/tests/renderer.clock.test.ts +158 -0
- package/src/tests/renderer.console-startup.test.ts +185 -0
- package/src/tests/renderer.control.test.ts +425 -0
- package/src/tests/renderer.core-slot-binding.test.ts +952 -0
- package/src/tests/renderer.cursor.test.ts +26 -0
- package/src/tests/renderer.destroy-during-render.test.ts +147 -0
- package/src/tests/renderer.focus-restore.test.ts +257 -0
- package/src/tests/renderer.focus.test.ts +294 -0
- package/src/tests/renderer.idle.test.ts +219 -0
- package/src/tests/renderer.input.test.ts +2237 -0
- package/src/tests/renderer.kitty-flags.test.ts +195 -0
- package/src/tests/renderer.mouse.test.ts +1274 -0
- package/src/tests/renderer.palette.test.ts +629 -0
- package/src/tests/renderer.selection.test.ts +49 -0
- package/src/tests/renderer.slot-registry.test.ts +684 -0
- package/src/tests/renderer.useMouse.test.ts +47 -0
- package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
- package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
- package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
- package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
- package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
- package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
- package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
- package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
- package/src/tests/runtime-plugin-support.fixture.ts +11 -0
- package/src/tests/runtime-plugin-support.test.ts +19 -0
- package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
- package/src/tests/runtime-plugin.fixture.ts +40 -0
- package/src/tests/runtime-plugin.test.ts +354 -0
- package/src/tests/scrollbox-culling-bug.test.ts +114 -0
- package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
- package/src/tests/scrollbox-hitgrid.test.ts +909 -0
- package/src/tests/scrollbox.test.ts +1530 -0
- package/src/tests/wrap-resize-perf.test.ts +276 -0
- package/src/tests/yoga-setters.test.ts +921 -0
- package/src/text-buffer-view.test.ts +705 -0
- package/src/text-buffer-view.ts +189 -0
- package/src/text-buffer.test.ts +347 -0
- package/src/text-buffer.ts +250 -0
- package/src/types.ts +161 -0
- package/src/utils.ts +88 -0
- package/src/zig/ansi.zig +268 -0
- package/src/zig/bench/README.md +50 -0
- package/src/zig/bench/buffer-draw-text-buffer_bench.zig +887 -0
- package/src/zig/bench/edit-buffer_bench.zig +476 -0
- package/src/zig/bench/native-span-feed_bench.zig +100 -0
- package/src/zig/bench/rope-markers_bench.zig +713 -0
- package/src/zig/bench/rope_bench.zig +514 -0
- package/src/zig/bench/styled-text_bench.zig +470 -0
- package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
- package/src/zig/bench/text-buffer-view_bench.zig +459 -0
- package/src/zig/bench/text-chunk-graphemes_bench.zig +273 -0
- package/src/zig/bench/utf8_bench.zig +799 -0
- package/src/zig/bench-utils.zig +431 -0
- package/src/zig/bench.zig +217 -0
- package/src/zig/buffer-methods.zig +211 -0
- package/src/zig/buffer.zig +2281 -0
- package/src/zig/build.zig +289 -0
- package/src/zig/build.zig.zon +16 -0
- package/src/zig/edit-buffer.zig +825 -0
- package/src/zig/editor-view.zig +802 -0
- package/src/zig/event-bus.zig +13 -0
- package/src/zig/event-emitter.zig +65 -0
- package/src/zig/file-logger.zig +92 -0
- package/src/zig/grapheme.zig +599 -0
- package/src/zig/lib.zig +1854 -0
- package/src/zig/link.zig +333 -0
- package/src/zig/logger.zig +43 -0
- package/src/zig/mem-registry.zig +125 -0
- package/src/zig/native-span-feed-bench-lib.zig +7 -0
- package/src/zig/native-span-feed.zig +708 -0
- package/src/zig/renderer.zig +1393 -0
- package/src/zig/rope.zig +1220 -0
- package/src/zig/syntax-style.zig +161 -0
- package/src/zig/terminal.zig +987 -0
- package/src/zig/test.zig +72 -0
- package/src/zig/tests/README.md +18 -0
- package/src/zig/tests/buffer-methods_test.zig +1109 -0
- package/src/zig/tests/buffer_test.zig +2557 -0
- package/src/zig/tests/edit-buffer-history_test.zig +271 -0
- package/src/zig/tests/edit-buffer_test.zig +1689 -0
- package/src/zig/tests/editor-view_test.zig +3299 -0
- package/src/zig/tests/event-emitter_test.zig +249 -0
- package/src/zig/tests/grapheme_test.zig +1304 -0
- package/src/zig/tests/link_test.zig +190 -0
- package/src/zig/tests/mem-registry_test.zig +473 -0
- package/src/zig/tests/memory_leak_regression_test.zig +159 -0
- package/src/zig/tests/native-span-feed_test.zig +1264 -0
- package/src/zig/tests/renderer_test.zig +1017 -0
- package/src/zig/tests/rope-nested_test.zig +712 -0
- package/src/zig/tests/rope_fuzz_test.zig +238 -0
- package/src/zig/tests/rope_test.zig +2362 -0
- package/src/zig/tests/segment-merge.test.zig +148 -0
- package/src/zig/tests/syntax-style_test.zig +557 -0
- package/src/zig/tests/terminal_test.zig +754 -0
- package/src/zig/tests/text-buffer-drawing_test.zig +3237 -0
- package/src/zig/tests/text-buffer-highlights_test.zig +666 -0
- package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
- package/src/zig/tests/text-buffer-segment_test.zig +320 -0
- package/src/zig/tests/text-buffer-selection_test.zig +1035 -0
- package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
- package/src/zig/tests/text-buffer-view_test.zig +3649 -0
- package/src/zig/tests/text-buffer_test.zig +2191 -0
- package/src/zig/tests/unicode-width-map.zon +3909 -0
- package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
- package/src/zig/tests/utf8_test.zig +4057 -0
- package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
- package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
- package/src/zig/tests/word-wrap-editing_test.zig +498 -0
- package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
- package/src/zig/text-buffer-iterators.zig +499 -0
- package/src/zig/text-buffer-segment.zig +404 -0
- package/src/zig/text-buffer-view.zig +1371 -0
- package/src/zig/text-buffer.zig +1180 -0
- package/src/zig/utf8.zig +1948 -0
- package/src/zig/utils.zig +9 -0
- package/src/zig-structs.ts +261 -0
- package/src/zig.ts +3884 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +27 -0
- package/3d/SpriteResourceManager.d.ts +0 -74
- package/3d/SpriteUtils.d.ts +0 -13
- package/3d/TextureUtils.d.ts +0 -24
- package/3d/ThreeRenderable.d.ts +0 -40
- package/3d/WGPURenderer.d.ts +0 -61
- package/3d/animation/ExplodingSpriteEffect.d.ts +0 -71
- package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +0 -76
- package/3d/animation/SpriteAnimator.d.ts +0 -124
- package/3d/animation/SpriteParticleGenerator.d.ts +0 -62
- package/3d/canvas.d.ts +0 -44
- package/3d/index.d.ts +0 -12
- package/3d/physics/PlanckPhysicsAdapter.d.ts +0 -19
- package/3d/physics/RapierPhysicsAdapter.d.ts +0 -19
- package/3d/physics/physics-interface.d.ts +0 -27
- package/3d.d.ts +0 -2
- package/3d.js +0 -34041
- package/3d.js.map +0 -155
- package/LICENSE +0 -21
- package/NativeSpanFeed.d.ts +0 -41
- package/Renderable.d.ts +0 -334
- package/animation/Timeline.d.ts +0 -126
- package/ansi.d.ts +0 -13
- package/buffer.d.ts +0 -111
- package/console.d.ts +0 -144
- package/edit-buffer.d.ts +0 -98
- package/editor-view.d.ts +0 -73
- package/index-9vwc3fg6.js +0 -12260
- package/index-9vwc3fg6.js.map +0 -42
- package/index-dcj62y8t.js +0 -20614
- package/index-dcj62y8t.js.map +0 -67
- package/index-f7n39gpy.js +0 -411
- package/index-f7n39gpy.js.map +0 -10
- package/index.d.ts +0 -23
- package/index.js +0 -478
- package/index.js.map +0 -9
- package/lib/KeyHandler.d.ts +0 -61
- package/lib/RGBA.d.ts +0 -25
- package/lib/ascii.font.d.ts +0 -508
- package/lib/border.d.ts +0 -51
- package/lib/bunfs.d.ts +0 -7
- package/lib/clipboard.d.ts +0 -17
- package/lib/clock.d.ts +0 -15
- package/lib/data-paths.d.ts +0 -26
- package/lib/debounce.d.ts +0 -42
- package/lib/detect-links.d.ts +0 -6
- package/lib/env.d.ts +0 -42
- package/lib/extmarks-history.d.ts +0 -17
- package/lib/extmarks.d.ts +0 -89
- package/lib/hast-styled-text.d.ts +0 -17
- package/lib/index.d.ts +0 -21
- package/lib/keymapping.d.ts +0 -25
- package/lib/objects-in-viewport.d.ts +0 -24
- package/lib/output.capture.d.ts +0 -24
- package/lib/parse.keypress-kitty.d.ts +0 -2
- package/lib/parse.keypress.d.ts +0 -26
- package/lib/parse.mouse.d.ts +0 -30
- package/lib/paste.d.ts +0 -7
- package/lib/queue.d.ts +0 -15
- package/lib/renderable.validations.d.ts +0 -12
- package/lib/scroll-acceleration.d.ts +0 -43
- package/lib/selection.d.ts +0 -63
- package/lib/singleton.d.ts +0 -7
- package/lib/stdin-parser.d.ts +0 -87
- package/lib/styled-text.d.ts +0 -63
- package/lib/terminal-capability-detection.d.ts +0 -30
- package/lib/terminal-palette.d.ts +0 -50
- package/lib/tree-sitter/assets/update.d.ts +0 -11
- package/lib/tree-sitter/client.d.ts +0 -47
- package/lib/tree-sitter/default-parsers.d.ts +0 -2
- package/lib/tree-sitter/download-utils.d.ts +0 -21
- package/lib/tree-sitter/index.d.ts +0 -8
- package/lib/tree-sitter/parser.worker.d.ts +0 -1
- package/lib/tree-sitter/parsers-config.d.ts +0 -53
- package/lib/tree-sitter/resolve-ft.d.ts +0 -5
- package/lib/tree-sitter/types.d.ts +0 -82
- package/lib/tree-sitter-styled-text.d.ts +0 -14
- package/lib/validate-dir-name.d.ts +0 -1
- package/lib/yoga.options.d.ts +0 -32
- package/parser.worker.js +0 -899
- package/parser.worker.js.map +0 -12
- package/plugins/core-slot.d.ts +0 -72
- package/plugins/registry.d.ts +0 -42
- package/plugins/types.d.ts +0 -34
- package/post/effects.d.ts +0 -147
- package/post/filters.d.ts +0 -65
- package/post/matrices.d.ts +0 -20
- package/renderables/ASCIIFont.d.ts +0 -52
- package/renderables/Box.d.ts +0 -81
- package/renderables/Code.d.ts +0 -78
- package/renderables/Diff.d.ts +0 -142
- package/renderables/EditBufferRenderable.d.ts +0 -237
- package/renderables/FrameBuffer.d.ts +0 -16
- package/renderables/Input.d.ts +0 -67
- package/renderables/LineNumberRenderable.d.ts +0 -78
- package/renderables/Markdown.d.ts +0 -185
- package/renderables/ScrollBar.d.ts +0 -77
- package/renderables/ScrollBox.d.ts +0 -124
- package/renderables/Select.d.ts +0 -115
- package/renderables/Slider.d.ts +0 -47
- package/renderables/TabSelect.d.ts +0 -96
- package/renderables/Text.d.ts +0 -36
- package/renderables/TextBufferRenderable.d.ts +0 -105
- package/renderables/TextNode.d.ts +0 -91
- package/renderables/TextTable.d.ts +0 -140
- package/renderables/Textarea.d.ts +0 -63
- package/renderables/TimeToFirstDraw.d.ts +0 -24
- package/renderables/__tests__/renderable-test-utils.d.ts +0 -12
- package/renderables/composition/VRenderable.d.ts +0 -16
- package/renderables/composition/constructs.d.ts +0 -35
- package/renderables/composition/vnode.d.ts +0 -46
- package/renderables/index.d.ts +0 -23
- package/renderables/markdown-parser.d.ts +0 -10
- package/renderer.d.ts +0 -419
- package/runtime-plugin-support.d.ts +0 -3
- package/runtime-plugin-support.js +0 -29
- package/runtime-plugin-support.js.map +0 -10
- package/runtime-plugin.d.ts +0 -16
- package/runtime-plugin.js +0 -16
- package/runtime-plugin.js.map +0 -9
- package/syntax-style.d.ts +0 -54
- package/testing/manual-clock.d.ts +0 -17
- package/testing/mock-keys.d.ts +0 -81
- package/testing/mock-mouse.d.ts +0 -38
- package/testing/mock-tree-sitter-client.d.ts +0 -23
- package/testing/spy.d.ts +0 -7
- package/testing/test-recorder.d.ts +0 -61
- package/testing/test-renderer.d.ts +0 -23
- package/testing.d.ts +0 -6
- package/testing.js +0 -697
- package/testing.js.map +0 -15
- package/text-buffer-view.d.ts +0 -42
- package/text-buffer.d.ts +0 -67
- package/types.d.ts +0 -139
- package/utils.d.ts +0 -14
- package/zig-structs.d.ts +0 -155
- package/zig.d.ts +0 -353
- /package/{assets → src/lib/tree-sitter/assets}/javascript/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/javascript/tree-sitter-javascript.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/injections.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/tree-sitter-markdown.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/typescript/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/typescript/tree-sitter-typescript.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/zig/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/zig/tree-sitter-zig.wasm +0 -0
|
@@ -0,0 +1,2660 @@
|
|
|
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
|
+
})
|