vadimdemedes / ink

🌈 React for interactive command-line apps
https://term.ink
MIT License
26.42k stars 594 forks source link

Scrolling #222

Open arcanis opened 5 years ago

arcanis commented 5 years ago

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?

vadimdemedes commented 5 years ago

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.

arcanis commented 5 years ago

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).

vadimdemedes commented 4 years ago

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

zaripych commented 4 years ago

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:

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.

arcanis commented 4 years ago

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.

vadimdemedes commented 4 years ago

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.

zaripych commented 4 years ago

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:

apatrida commented 4 years ago

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.

OnkelTem commented 3 years ago

@zaripych Any progress so far?

zaripych commented 3 years ago

@OnkelTem With recent changes in Ink it's now possible for me to measure elements inside a scroll container. But I still use virtualization (limiting number of elements I mount inside scroll view) instead of using overflow: hidden approach. I think virtualization is reasonable and more scalable alternative considering number of elements inside scroll view can be high. I have to set flex-shrink: 0 on children so they won't shrink when pushed by boundaries of scroll view. I could dig up some code examples for you a little later.

devsnek commented 3 years ago

Let me know if there's anything I can do to help here. I'm currently working on adding interactivity to an ink app I'm working on (for example mouse events) and scrolling is on my todo list.

dustinlacewell commented 3 years ago

Hoping to hear more on this front.

abejfehr commented 1 year ago

I'd just like to share that Textual has scrolling views that even have scrollbars, and buttons, etc.

rix0rrr commented 1 year ago

Oh my god Textual looks amazing! Why does everything in JS-land have to be so barebones with add-on packages for every little thing that ultimately turn out to be on incompatible version ranges of shared libraries? 😭

jedwards1211 commented 3 months ago

@zaripych

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.

How comprehensive can this solution be though? Would it be able to handle descendant elements spontaneously resizing? Does yoga provide bubbling layout change events?

Seems to me that it would be impossible to do in userland as well as if ink were built on top of some DOM-like engine that handles scrolling, and does text rendering from the DOM nodes.