Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rerender on resize #304

Merged
merged 2 commits into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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