diff --git a/src/components/Box.tsx b/src/components/Box.tsx index c22a2a107..90e809c46 100644 --- a/src/components/Box.tsx +++ b/src/components/Box.tsx @@ -2,7 +2,7 @@ import React, {forwardRef, PropsWithChildren} from 'react'; import {Except} from 'type-fest'; import {Styles} from '../styles'; -import {DOMElement} from '../dom'; +import {DirectRenderFunc, DOMElement} from '../dom'; export type Props = Except & { /** @@ -46,13 +46,18 @@ export type Props = Except & { * @default 0 */ readonly paddingY?: number; + + /** + * Specify a custom render function that will be called by the renderer before border and children are rendered. + */ + readonly unsafeDirectRender?: DirectRenderFunc; }; /** * `` is an essential Ink component to build your layout. It's like `
` in the browser. */ const Box = forwardRef>( - ({children, ...style}, ref) => { + ({children, unsafeDirectRender, ...style}, ref) => { const transformedStyle = { ...style, marginLeft: style.marginLeft || style.marginX || style.margin || 0, @@ -66,7 +71,11 @@ const Box = forwardRef>( }; return ( - + {children} ); diff --git a/src/dom.ts b/src/dom.ts index 3b3ae18b9..0cc2ff0b8 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -4,12 +4,21 @@ import applyStyles, {Styles} from './styles'; import wrapText from './wrap-text'; import squashTextNodes from './squash-text-nodes'; import {OutputTransformer} from './render-node-to-output'; +import Output from './output'; + +export type DirectRenderFunc = ( + x: number, + y: number, + node: DOMNode, + output: Output +) => void; interface InkNode { parentNode: DOMElement | null; yogaNode?: Yoga.YogaNode; internal_static?: boolean; style: Styles; + internal_pre_render?: DirectRenderFunc; } export const TEXT_NAME = '#text'; diff --git a/src/global.d.ts b/src/global.d.ts index c161adc92..dbccc80f3 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,6 +1,6 @@ import {ReactNode, Key, LegacyRef} from 'react'; import {Except} from 'type-fest'; -import {DOMElement} from './dom'; +import {DirectRenderFunc, DOMElement} from './dom'; import {Styles} from './styles'; declare global { @@ -18,6 +18,7 @@ declare namespace Ink { key?: Key; ref?: LegacyRef; style?: Except; + unsafeDirectRender?: DirectRenderFunc; } interface Text { diff --git a/src/reconciler.ts b/src/reconciler.ts index bae2cb462..907bddf20 100644 --- a/src/reconciler.ts +++ b/src/reconciler.ts @@ -114,6 +114,8 @@ export default createReconciler< node.internal_transform = value as OutputTransformer; } else if (key === 'internal_static') { node.internal_static = true; + } else if (key === 'unsafeDirectRender') { + node.internal_pre_render = value as any; } else { setAttribute(node, key, value as DOMNodeAttribute); } @@ -230,6 +232,8 @@ export default createReconciler< node.internal_transform = value as OutputTransformer; } else if (key === 'internal_static') { node.internal_static = true; + } else if (key === 'unsafeDirectRender') { + node.internal_pre_render = value as any; } else { setAttribute(node, key, value as DOMNodeAttribute); } diff --git a/src/render-node-to-output.ts b/src/render-node-to-output.ts index cb0e05fee..96a9c273f 100644 --- a/src/render-node-to-output.ts +++ b/src/render-node-to-output.ts @@ -89,6 +89,10 @@ const renderNodeToOutput = ( } if (node.nodeName === 'ink-box') { + if (node.internal_pre_render) { + node.internal_pre_render(x, y, node, output); + } + renderBorder(x, y, node, output); } diff --git a/test/render.tsx b/test/render.tsx index f15cab26a..6a600813b 100644 --- a/test/render.tsx +++ b/test/render.tsx @@ -8,6 +8,7 @@ import boxen from 'boxen'; import delay from 'delay'; import {render, Box, Text} from '../src'; import createStdout from './helpers/create-stdout'; +import {DirectRenderFunc} from '../src/dom'; const term = (fixture: string, args: string[] = []) => { let resolve: (value?: unknown) => void; @@ -142,3 +143,24 @@ test('rerender on resize', async t => { unmount(); t.is(stdout.listeners('resize').length, 0); }); + +test('manual render', t => { + const stdout = createStdout(10); + + const custRender: DirectRenderFunc = (x, y, node, output) => { + const width = node.yogaNode!.getComputedWidth(); + output.write(x, y, '$'.repeat(width), {transformers: []}); + }; + + const Test = () => ( + + Test + + ); + + const {unmount} = render(, {stdout}); + + t.is(stripAnsi(stdout.write.firstCall.args[0]), 'Test$$$$$$\n'); + + unmount(); +});