Ink: Scrolling

Created on 29 Aug 2019  ·  8Comments  ·  Source: vadimdemedes/ink

Hello! I've been playing with Ink yesterday, and I really like where you're going. I've worked on something similar before, but your approach seems much simpler and less labyrinthine. Kudos!

One particular thing I'm finding myself missing, however, are scrollable containers. I anticipate that printing a lot of data will eventually cause my display to go beyond the height of a regular terminal (particularly when working with lists of interactive elements), so scroll will be quite important.

I'm trying to think how would one implement this feature through userland blocks, but I don't see how it could be done - I remember that implementing this in my library has required to first support overflows, meaning computing intersections between the various nodes printed on screen.

Do you have some thoughts about this topic?

question

Most helpful comment

Hey @vadimdemedes ... I agree that focus management might be complicated. However, at the moment, it can be implemented by the users of the library, using useInput hook and a couple of custom baked hooks that will have to be used by every input element.

The layout issues that I found, however, can only be fixed through exposing yoga API from Box instance (similar to unstable_getComputedWidth) methods or through introduction of additional helper Context. So, they can only be fixed by changing ink.

Even if ink doesn’t provide a ScrollBox component I think if we fix those issues the community would be able to create their own version of ScrollBox.

I was able to change the experimental renderer to clip sub-elements of a Box that I had overflow=“hidden” set in the tree. I’m still at early stages and need to debug and measure performance impact and test, but I think I should be able to create a PR.

The issues that I would like to address are:

  • being able to measure Box elements
  • being able to clip sub-elements of a Box by setting overflow=“hidden”

All 8 comments

Hi, thanks! I want to merge https://github.com/vadimdemedes/ink/pull/210 to solve this problem, I just want to test it more extensively. With this PR seems you won't need scrollable containers anymore.

Hm but that doesn't solve the issue for interactive elements, right? For example, if we imagine a list of radio buttons, the ones beyond the top of the screen would never be visible without first-party scrolling (terminals support some basic scroll, but typing anything would put back the scroll to the bottom).

Yep, in this case components have to implement their own scrolling, similar to https://github.com/vadimdemedes/ink-select-input.

Maybe what @arcanis means here is something of a general purpose scrolling component that can wrap any number of components as children and allow scrolling through it. I tried to write something like that and bumped into a few issues:

  • there seem to be no way to measure height of the child nodes (it is possible in react-native that also uses yoga, using measure functions)
  • there seem to be no way to determine height of the viewport (the scroll view component itself), the height must be fixed
  • there is no parameter similar to overflow like in react-native where all sub-components of a Box would have been clipped. neither there is zIndex. as a result if the scroll view is just next to a header, scrolling through elements will render on top of the header
interface IScrollViewProps {
  totalHeight: number;
  viewportHeight: number;
}

export const VerticalScrollView: React.FC<IScrollViewProps> = props => {
  const thumbHeight = 3;

  const [offset, setOffsetCore] = React.useState(0);

  const thumbPosition =
    (offset / (props.totalHeight - (props.viewportHeight - thumbHeight))) *
    props.viewportHeight;

  const setOffset = (changer: (value: number) => number) => {
    return setOffsetCore(value => {
      const next = changer(value);
      return Math.min(
        Math.max(next, 0),
        props.totalHeight - props.viewportHeight - 1
      );
    });
  };

  useInput((text, key) => {
    if (key.upArrow || text === 'OA') {
      setOffset(value => value - 1);
    }
    if (key.downArrow || text === 'OB') {
      setOffset(value => value + 1);
    }
  });

  return (
    <Box flexDirection="row" flexGrow={1} justifyContent="space-between">
      <Box marginY={-offset}>{props.children}</Box>
      <Box width={1} marginTop={thumbPosition} textWrap="wrap">
        |||
      </Box>
    </Box>
  );
};

In my example below the header gets erased by the list as we scroll down:

export const App: React.FC<{}> = () => {
  const items = range(100);
  const screen = screenSize();
  const viewportHeight = screen.height - 5;
  const totalHeight = items.length;

  return (
    <Box {...screen} flexDirection="column">
      <Box flexShrink={0}>
        <Color red>Header should stay visible</Color>
      </Box>
      <Box flexShrink={0}>
        <Color red>Header should stay visible</Color>
      </Box>
      <Box flexGrow={1} flexDirection="column">
        <VerticalScrollView
          totalHeight={totalHeight}
          viewportHeight={viewportHeight}
        >
          <Box flexDirection="column" flexGrow={1}>
            {items.map((item, i) => (
              <Box key={i} flexDirection="row" flexShrink={0}>
                <Text>{item.toString(10)}</Text>
              </Box>
            ))}
          </Box>
        </VerticalScrollView>
      </Box>
      <Box flexShrink={0}>
        <Color red>Footer should stay visible</Color>
      </Box>
      <Box flexShrink={0}>
        Footer should stay visible; With viewport height{' '}
        <Color green>{viewportHeight}</Color> and Screen height{' '}
        <Color green>{screen.height}</Color>
      </Box>
    </Box>
  );
};

@vadimdemedes do you think Ink could be modified to support these features?

Off the topic, it also seemed like I was missing alignSelf property and stretch option for alignItems property.

Maybe what @arcanis means here is something of a general purpose scrolling component that can wrap any number of components as children and allow scrolling through it. I tried to write something like that and bumped into a few issues:

Yes exactly. Fixed-size single-row elements are easy enough to scroll, but as soon as you get more complex setups it becomes kind of a pain - even fixed-size multi-row elements aren't trivial to implement this way since you then need to find a way to make them partially visible (or you take a shortcut and make the scroll size equal to the element size).

In my case for example, I have a bunch of elements like this. When using Ink on large projects, the list goes well past the size of the screen and most elements are hidden:

image

Lack of scrolling is maybe the single biggest issue we have we Ink.

I don't have a solution for it short-term and browser-like focus management is also required to make scrolling work, which certainly complicates things. Until Ink provides some sort of feature for this, custom scrolling implementation is the way to go for this sort of UI.

Hey @vadimdemedes ... I agree that focus management might be complicated. However, at the moment, it can be implemented by the users of the library, using useInput hook and a couple of custom baked hooks that will have to be used by every input element.

The layout issues that I found, however, can only be fixed through exposing yoga API from Box instance (similar to unstable_getComputedWidth) methods or through introduction of additional helper Context. So, they can only be fixed by changing ink.

Even if ink doesn’t provide a ScrollBox component I think if we fix those issues the community would be able to create their own version of ScrollBox.

I was able to change the experimental renderer to clip sub-elements of a Box that I had overflow=“hidden” set in the tree. I’m still at early stages and need to debug and measure performance impact and test, but I think I should be able to create a PR.

The issues that I would like to address are:

  • being able to measure Box elements
  • being able to clip sub-elements of a Box by setting overflow=“hidden”

I'm running into this as well, I need to scroll at the size of the screen minus the area taken by boxes above/below the component. I can implement scrolling based on some fixed height, but have no good way of knowing the sizes of boxes.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

treshugart picture treshugart  ·  4Comments

danikaze picture danikaze  ·  5Comments

HMR
konsumer picture konsumer  ·  3Comments

zkat picture zkat  ·  7Comments

gc picture gc  ·  6Comments