Skip to content

Commit

Permalink
Rerender on resize (#304)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadim Demedes authored Jun 18, 2020
1 parent 2bcb4c0 commit 155e1a0
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 131 deletions.
27 changes: 18 additions & 9 deletions src/ink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import ansiEscapes from 'ansi-escapes';
import originalIsCI from 'is-ci';
import autoBind from 'auto-bind';
import reconciler from './reconciler';
import createRenderer from './renderer';
import type {Renderer} from './renderer';
import render from './renderer';
import signalExit from 'signal-exit';
import patchConsole from 'patch-console';
import * as dom from './dom';
Expand Down Expand Up @@ -41,9 +40,9 @@ export default class Ink {
// This variable is used only in debug mode to store full static output
// so that it's rerendered every time, not just new static parts, like in non-debug mode
private fullStaticOutput: string;
private readonly renderer: Renderer;
private exitPromise?: Promise<void>;
private restoreConsole?: () => void;
private readonly unsubscribeResize?: () => void;

constructor(options: Options) {
autoBind(this);
Expand All @@ -59,11 +58,6 @@ export default class Ink {
});

this.rootNode.onImmediateRender = this.onRender;

this.renderer = createRenderer({
terminalWidth: options.stdout.columns
});

this.log = logUpdate.create(options.stdout);
this.throttledLog = options.debug
? this.log
Expand Down Expand Up @@ -100,6 +94,14 @@ export default class Ink {
if (options.patchConsole) {
this.patchConsole();
}

if (!isCI) {
options.stdout.on('resize', this.onRender);

this.unsubscribeResize = () => {
options.stdout.off('resize', this.onRender);
};
}
}

resolveExitPromise: () => void = () => {};
Expand All @@ -111,7 +113,10 @@ export default class Ink {
return;
}

const {output, outputHeight, staticOutput} = this.renderer(this.rootNode);
const {output, outputHeight, staticOutput} = render(
this.rootNode,
this.options.stdout.columns
);

// If <Static> output isn't empty, it means new children have been added to it
const hasStaticOutput = staticOutput && staticOutput !== '\n';
Expand Down Expand Up @@ -231,6 +236,10 @@ export default class Ink {
this.restoreConsole();
}

if (typeof this.unsubscribeResize === 'function') {
this.unsubscribeResize();
}

// CIs don't handle erasing ansi escapes well, so it's better to
// only render last frame of non-static output
if (isCI) {
Expand Down
73 changes: 33 additions & 40 deletions src/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,54 @@
import Yoga from 'yoga-layout-prebuilt';
import renderNodeToOutput from './render-node-to-output';
import Output from './output';
import {setStyle} from './dom';
import type {DOMElement} from './dom';

export type Renderer = (
node: DOMElement
) => {
interface Result {
output: string;
outputHeight: number;
staticOutput: string;
};

export default ({terminalWidth = 100}: {terminalWidth: number}): Renderer => {
return (node: DOMElement) => {
setStyle(node, {
width: terminalWidth
});
}

if (node.yogaNode) {
node.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);

const output = new Output({
width: node.yogaNode.getComputedWidth(),
height: node.yogaNode.getComputedHeight()
});
export default (node: DOMElement, terminalWidth: number): Result => {
node.yogaNode!.setWidth(terminalWidth);

renderNodeToOutput(node, output, {skipStaticElements: true});
if (node.yogaNode) {
node.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);

let staticOutput;
const output = new Output({
width: node.yogaNode.getComputedWidth(),
height: node.yogaNode.getComputedHeight()
});

if (node.staticNode?.yogaNode) {
staticOutput = new Output({
width: node.staticNode.yogaNode.getComputedWidth(),
height: node.staticNode.yogaNode.getComputedHeight()
});
renderNodeToOutput(node, output, {skipStaticElements: true});

renderNodeToOutput(node.staticNode, staticOutput, {
skipStaticElements: false
});
}
let staticOutput;

const {output: generatedOutput, height: outputHeight} = output.get();
if (node.staticNode?.yogaNode) {
staticOutput = new Output({
width: node.staticNode.yogaNode.getComputedWidth(),
height: node.staticNode.yogaNode.getComputedHeight()
});

return {
output: generatedOutput,
outputHeight,
// Newline at the end is needed, because static output doesn't have one, so
// interactive output will override last line of static output
staticOutput: staticOutput ? `${staticOutput.get().output}\n` : ''
};
renderNodeToOutput(node.staticNode, staticOutput, {
skipStaticElements: false
});
}

const {output: generatedOutput, height: outputHeight} = output.get();

return {
output: '',
outputHeight: 0,
staticOutput: ''
output: generatedOutput,
outputHeight,
// Newline at the end is needed, because static output doesn't have one, so
// interactive output will override last line of static output
staticOutput: staticOutput ? `${staticOutput.get().output}\n` : ''
};
}

return {
output: '',
outputHeight: 0,
staticOutput: ''
};
};
36 changes: 8 additions & 28 deletions test/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
useStdin,
render
} from '../src';
import createStdout from './helpers/create-stdout';

test('text', t => {
const output = renderToString(<Text>Hello World</Text>);
Expand Down Expand Up @@ -191,10 +192,7 @@ test('fail when text node is not within <Text> component', t => {
});

test('remesure text dimensions on text change', t => {
const stdout = {
write: spy(),
columns: 100
};
const stdout = createStdout();

const {rerender} = render(
<Box>
Expand Down Expand Up @@ -309,10 +307,7 @@ test('static output', t => {
});

test('skip previous output when rendering new static output', t => {
const stdout = {
write: spy(),
columns: 100
};
const stdout = createStdout();

const Dynamic: FC<{items: string[]}> = ({items}) => (
<Static items={items}>{item => <Text key={item}>{item}</Text>}</Static>
Expand All @@ -337,10 +332,7 @@ test('ensure wrap-ansi doesn’t trim leading whitespace', t => {
});

test('replace child node with text', t => {
const stdout = {
write: spy(),
columns: 100
};
const stdout = createStdout();

const Dynamic = ({replace}) => (
<Text>{replace ? 'x' : <Text color="green">test</Text>}</Text>
Expand All @@ -359,10 +351,7 @@ test('replace child node with text', t => {

// See https://github.com/vadimdemedes/ink/issues/145
test('disable raw mode when all input components are unmounted', t => {
const stdout = {
write: spy(),
columns: 100
};
const stdout = createStdout();

const stdin = new EventEmitter();
stdin.setEncoding = () => {};
Expand Down Expand Up @@ -427,10 +416,7 @@ test('disable raw mode when all input components are unmounted', t => {
});

test('setRawMode() should throw if raw mode is not supported', t => {
const stdout = {
write: spy(),
columns: 100
};
const stdout = createStdout();

const stdin = new EventEmitter();
stdin.setEncoding = () => {};
Expand Down Expand Up @@ -486,10 +472,7 @@ test('setRawMode() should throw if raw mode is not supported', t => {
});

test('render different component based on whether stdin is a TTY or not', t => {
const stdout = {
write: spy(),
columns: 100
};
const stdout = createStdout();

const stdin = new EventEmitter();
stdin.setEncoding = () => {};
Expand Down Expand Up @@ -578,10 +561,7 @@ test('render all frames if CI environment variable equals false', async t => {
});

test('reset prop when it’s removed from the element', t => {
const stdout = {
write: spy(),
columns: 100
};
const stdout = createStdout();

const Dynamic = ({remove}) => (
<Box
Expand Down
25 changes: 11 additions & 14 deletions test/errors.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable unicorn/string-content */
import React from 'react';
import test from 'ava';
import {spy} from 'sinon';
import patchConsole from 'patch-console';
import stripAnsi from 'strip-ansi';
import {render} from '../src';
import createStdout from './helpers/create-stdout';

let restore;

Expand All @@ -17,10 +17,7 @@ test.after(() => {
});

test('catch and display error', t => {
const stdout = {
columns: 100,
write: spy()
};
const stdout = createStdout();

const Test = () => {
throw new Error('Oh no');
Expand All @@ -34,17 +31,17 @@ test('catch and display error', t => {
'',
' ERROR Oh no',
'',
' test/errors.tsx:26:9',
' test/errors.tsx:23:9',
'',
' 23: };',
' 24:',
' 25: const Test = () => {',
" 26: throw new Error('Oh no');",
' 27: };',
' 28:',
' 29: render(<Test />, {stdout});',
' 20: const stdout = createStdout();',
' 21:',
' 22: const Test = () => {',
" 23: throw new Error('Oh no');",
' 24: };',
' 25:',
' 26: render(<Test />, {stdout});',
'',
' - Test (test/errors.tsx:26:9)'
' - Test (test/errors.tsx:23:9)'
]
);
});
6 changes: 1 addition & 5 deletions test/focus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import delay from 'delay';
import test from 'ava';
import {spy} from 'sinon';
import {render, Box, Text, useFocus, useFocusManager} from '..';

const createStdout = () => ({
write: spy(),
columns: 100
});
import createStdout from './helpers/create-stdout';

const createStdin = () => {
const stdin = new EventEmitter();
Expand Down
19 changes: 19 additions & 0 deletions test/helpers/create-stdout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import EventEmitter from 'events';
import {spy} from 'sinon';

// Fake process.stdout
interface Stream extends EventEmitter {
output: string;
columns: number;
write(str: string): void;
get(): string;
}

export default (columns?: number): Stream => {
const stdout = new EventEmitter();
stdout.columns = columns ?? 100;
stdout.write = spy();
stdout.get = () => stdout.write.lastCall.args[0];

return stdout;
};
29 changes: 4 additions & 25 deletions test/helpers/render-to-string.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,17 @@
import {render} from '../../src';

// Fake process.stdout
interface Stream {
output: string;
columns: number;
write(str: string): void;
get(): string;
}

const createStream: (options: {columns: number}) => Stream = ({columns}) => {
let output = '';
return {
output,
columns,
write(str: string) {
output = str;
},
get() {
return output;
}
};
};
import createStdout from './create-stdout';

export const renderToString: (
node: JSX.Element,
options?: {columns: number}
) => string = (node, options = {columns: 100}) => {
const stream = createStream(options);
const stdout = createStdout(options.columns);

render(node, {
// @ts-ignore
stdout: stream,
stdout,
debug: true
});

return stream.get();
return stdout.get();
};
7 changes: 1 addition & 6 deletions test/reconciler.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import React, {Suspense} from 'react';
import test from 'ava';
import chalk from 'chalk';
import {spy} from 'sinon';
import {Box, Text, render} from '../src';

const createStdout = () => ({
write: spy(),
columns: 100
});
import createStdout from './helpers/create-stdout';

test('update child', t => {
const Test = ({update}) => <Text>{update ? 'B' : 'A'}</Text>;
Expand Down
Loading

0 comments on commit 155e1a0

Please sign in to comment.