See https://github.com/vadimdemedes/ink/pull/3#discussion_r126292283.
The problem is demonstrated perfectly when rendering a progress bar:
<div>
<Label/>
<ProgressBar/>
</div>
In order to render a progress bar that takes entire terminal space (width), <ProgressBar> needs to know how much space <Label> takes.
I'm short of ideas on how to implement this properly, so any thoughts are welcome!
I think figuring this out would also solve the nested-div-newline problem.
Perhaps this is overcomplicating things, but introducing a View using yoga-layout could solve this by introducing a full layout engine. I am not quite sure how one would go about building this. Reading and understanding the code of projects using yoga-layout should help, though:
Is this something you'd be interested in?
I think this is definitely interesting, but might be an overkill for now. I still hope there will be a simple solution to that issue.
I'd like to give this one a try. What exactly are we looking for? Should it be that every element past the first one should render in less space than the entire terminal based on the previous elements?
Should that happen for elements in a div? or in general?
Also, would it make sense to automatically cut anything that goes over the terminal length for each line (instead of creating another line)? Or just let the developer take care of making sure all lines are shorter than the total available space?
I'd like to give this one a try. What exactly are we looking for? Should it be that every element past the first one should render in less space than the entire terminal based on the previous elements?
I thought each component would have access to these props:
x - distance from the left side of terminaly - distance from the first row of outputI think exposing these props would allow components to calculate the available space in the current row. Ink would also have to rerender the tree whenever terminal is resized. This is ideal implementation, but might be complex to implement.
Should that happen for elements in a div? or in general?
I think all elements should have access to these props.
Also, would it make sense to automatically cut anything that goes over the terminal length for each line (instead of creating another line)? Or just let the developer take care of making sure all lines are shorter than the total available space?
I think given the props above, developer should decide themselves whether to cut the output or not.
I just had an idea to perhaps simplify the first implementation and experiment:
<SpaceAware>
<SpaceAware.Item>
<Color red>Progress: </Color>
</SpaceAware.Item>
<SpaceAware.Item>
{({ spaceAvailable }) => (
<ProgressBar width={spaceAvailable} value={0.5}/>
)}
</SpaceAware.Item>
</SpaceAware>
Naming isn't final of course, but the idea is this: SpaceAware renders to string via renderToString() each SpaceAware.Item child and calculates spaceAvailable for each of those and pass it down via render function. I realize it's not the prettiest implementation, but it's a start and could serve as a temporary solution until the "complete" system of x and y coordinates is implemented.
@karaggeorge @sindresorhus What do you think?
@vadimdemedes I'm a bit confused with the x and y system. How can a component (like ProgressBar) calculate how wide it has to be just using the proposed x and y. I think maybe a left and right type of prop my help more. left being the x and right being the maximum space left for the component to take on the terminal. Actually, that would make more sense if only the right-most component received the right.
It might be also weird to use renderToString() to calculate the size, but the components will need these new props to render in the first place.
The x and y as described would actually help a lot if we want to only update a specific component that updated instead of the whole tree.
Another idea might be having the components pass a flex: true prop in a surrounding component which would then pass the available size left on the screen to its child after all non-flex components have been rendered. If more than one flex components exist in one line, they would split the space between them. Thoughts?
I'm a bit confused with the x and y system. How can a component (like ProgressBar) calculate how wide it has to be just using the proposed x and y.
You can get the available space using terminal width - x = available width. The y coordinate is for placing mouse cursor, you can ignore it for this example.
I think having a flex prop would be complicated to explain and use.
Let me play with my idea and see if it even works. I'll submit it as a PR and ping you.
@vadimdemedes
You can get the available space using terminal width - x = available width
that would be true only if the component is the last one on the line, right?
Yes, I totally missed that, good catch.
@karaggeorge I implemented my idea but indeed is limited to 2 components on one row, side by side. I rethinked your idea with flex and I think it's the best way to solve this. My idea is to implement an alternative of CSS flexbox in Ink. That way layout is clearly determined and straightforward to understand. My thoughts:
// Progress: [======] (ProgressBar takes all the available space)
<Box>
<Box>
Progress:
</Box>
<Box flex={1}>
<ProgressBar/>
</Box>
<Box>
15%
</Box>
</Box>
<Box height={20} alignItems="center" justifyContent="center">
<HelloWorld/>
</Box>
It's indeed going to be a basic reimplementation of CSS flexbox, but using rows and columns instead of px.
@vadimdemedes Yeah, CSS flexbox is where I got that idea. I think it would make sense in this case, and it solves a couple of problems. What are you thinking about a full set of props? I was thinking just the flex-grow, but I see you incorporated more of them.
I'm wondering about the height. How would that display with the children of that element? Would it be a new line before and after the box? Like a div with a set height?
What are you thinking about a full set of props?
I think we should start with the minimum set first and expand over time.
I'm wondering about the height. How would that display with the children of that element? Would it be a new line before and after the box? Like a div with a set height?
Exactly 馃檪
@vadimdemedes
I think that makes sense. I'll start working on PR. Do we want it to be a prop passed in to any component? Or do we want to have a wrapper that does it for the children? The way you had it before:
<SpaceAware>
<A/>
</SpaceAware>
or <Flex/> as a wrapper, which would lead to A receiving an availableSpace prop.
@karaggeorge Sorry for late response. I thought we could go with https://github.com/vadimdemedes/ink/issues/5#issuecomment-379884409 to provide a complete layout solution. I tried implementing <SpaceAware> and it looks more like a hack than a solution.
It's indeed going to be a basic reimplementation of CSS flexbox, but using rows and columns instead of px.
@vadimdemedes Does it make sense to explore yoga-layout to do this?
Yoga is built with cross platform in mind. To ensure Yoga can be used anywhere, it was written in portable C/C++ and has a low number of dependencies and small binary size. This means Yoga can be used on iOS and Android, sharing knowledge, and potentially code, between platforms.
@marionebl I guess this means that Ink will have to include a native dependency, which I'd like to avoid.
Yoga has an asm.js build for browsers, which we could use in Node.js to not need a native dependency: https://github.com/facebook/yoga#build-platforms
I tried to implement something similar.
Just a simple <AppTitle> component, which renders a colored bar with the centered title.
The problem is that Box doesn't render bgColor so I need to do something like this:
export function AppTitle(props: Props) {
const text = props.children;
const width = props.width;
const middle = Math.floor((width - text.length)/ 2) + text.length;
return (
<Color bgBlue white bold>
{text.padStart(middle, ' ').padEnd(width, ' ')}
</Color>
);
}
Works like a charm. width is passed as a prop (which I think I could automatically read from stdout via process or <StdoutContext>, but I'm getting it from redux state so the component it's re-rendered when the window is resized.
But there's a problem (of course), and it's that the component is rendered multiple times instead of being replaced (probably a different issue #153 ?)
Going to close this issue, as there's a <Box flexGrow={1}> component which will fill all available space and https://github.com/vadimdemedes/ink/issues/168 is going to introduce an API to measure <Box> width.
Most helpful comment
Yoga has an asm.js build for browsers, which we could use in Node.js to not need a native dependency: https://github.com/facebook/yoga#build-platforms