tui-rs icon indicating copy to clipboard operation
tui-rs copied to clipboard

How to scroll Paragraph when appending to the end?

Open svenstaro opened this issue 7 years ago • 10 comments

I have a long multi-line string that I continuously append onto and I'd like Paragraph to append to the end. I'd then like Paragraph to scroll down so that no lines get hidden from the newly appended text. However, I can't know how much to scroll because Paragraph doesn't yield any information about its layouted text.

svenstaro avatar Nov 17 '18 18:11 svenstaro

Right now scrolling is pretty much O(n). Paragraph essentially behaves as a pager (e.g. less) - takes something that's essentially an iterator over characters and reads only as much as it needs to.

In the text below I only consider text reflow with word wrapping, as of https://github.com/fdehau/tui-rs/pull/103 - the case with line truncation cannot be more diffcult anyway.

1. Scroll to the bottom

It would be possible to implement a special "scroll to the bottom" mode by storing the last height lines in a circular buffer and displaying them when we hit the bottom - but this would still require scrolling the entire thing.

If we make the text iterator double-ended (rather - potentially double-ended), we'd "only" have to keep working our way from the end to the previous \n, compose it into nicely wrapped lines, store, and repeat from there until we've got enough lines to cover the text field. In the worst case there would be no \n and we'd have to seek to the beginning and do reflow from there. Similarly, scrolling to the bottom minus N lines would also work.

2. Scroll to X% of the text, know how far you've scrolled to from the offset, scrollbars

None of the above makes the above easy - for these you have to know the total height of the text (open a large file with less and press ^g to see it struggle).

Making the text random-access doesn't work for new lines - Text::raw and Text::styled still contain strings, which are not random-access and character widths vary.

The extra options I can see:

  • allow "pre-wrapped" text, essentially a vector of lines which will be truncated if they exceed the text area width.
  • allow the user to control what scrollbars show (assuming they know which they will probably not)
  • ...?

3. Since Paragraph consumes an iterator of Text::raw and Text::Styled and performs flattening internally, why not flatten it externally and make it consume it flattened so it can work like a proper pager?.

Text reflow would then operate on the flattened Text iterator and Paragraph would consume the result of word wrapping, shifting the logic away from the Paragraph.

karolinepauls avatar Dec 11 '18 12:12 karolinepauls

Another TODO when implementing this - make scroll usize rather than u16, possibly add vertical scrolling.

karolinepauls avatar Dec 14 '18 01:12 karolinepauls

I'd like to +1 this request. I'm currently working on a fork that will implement some kind of a solution for this problem. Will post in here, and if @fdehau or @karolinepauls thinks its useful, i'm happy to try and clean it up and merge upstream.

clevinson avatar Dec 10 '19 19:12 clevinson

A more elegant solution seemed to be more than I was willing off to attempt in one evening. Spent a lot of time thinking of switching the LineComposer to a streaming iterator, and then possibly a DoubleEndedStreamingIterator... but I ended up just adding a collect_lines() method to LineComposer, and the ability to have scroll occur from either the top or the bottom of the content.

@svenstaro I also added an example in my branch so you can clone & take a look if you want to see how it works and do a similar workaround in your project: https://github.com/clevinson/tui-rs/blob/scroll-from-bottom/examples/paragraph.rs#L93-L99

Happy to continue work on this and clean it up into something more reasonable if you see a preferred path forward.. @karolinepauls

clevinson avatar Dec 11 '19 01:12 clevinson

@fdehau This is now cleaned up, enabling two kinds of "scroll modes":

  • ScrollMode::Normal: Basically the existing mode, scroll, the integer scroll value indicates now many lines to skip over when iterating through LineComposer's next_line() call
  • ScrollMode::Tail: The new mode I added. Functionality is tailored to my specific use case, but i think others may enjoy this behavior as well.

In the Tail mode of scroll, the scroll_offset (u16) parameter indicates scroll offset from the bottom of the content, as opposed to scrolling from the top. There are a few different cases I cover: 1. Content is fully contained within text_area

  • scroll_offset == 0: This renders as a normal (float content to top) type of view. Any
  • scroll_offset > 0: This scrolls the content down, revealing additional blank space above the content, or a scroll_overflow_char visible on the left side of the text_area, repeated on each line above the first content line.

2. Content is longer than text_area

  • scroll_offset == 0: Functions as if scrolled to the bottom of the content. The last line in the text_area is the last line of the content.
  • scroll_offset > 0: Indicates number of lines scrolled from the bottom, scrolling up to reveal the rest of the content.

clevinson avatar Dec 12 '19 21:12 clevinson

I just ran into the lack of this feature while working on a project with this library (trying to use paragraph to display wrapped log output while always displaying the most recent log line at the bottom). It would be super nice to see support for this situation land!

EDIT: I ended up pre-wrapping the text with https://github.com/mgeisler/textwrap and just using the list widget to display it.

Cldfire avatar May 05 '20 05:05 Cldfire

Unfortunately doing this manually outside of the tui crate with the textwrap library is a lot more difficult now that Spans are a thing in the new 0.10 release. Handling wrapping of a line composed of multiple Spans with different styles is going to be a pretty big headache.

Not so bad if you restrict lines to being a single style (but of course I want lines with multiple styles :stuck_out_tongue:).

EDIT: example impl of line wrapping here (with the constraint that lines are built from un-styled strings)

Cldfire avatar Jul 19 '20 15:07 Cldfire

@clevinson Do you still intent to make a pr on this repo to merge your fix upstream or do you think #349 handles your needs?

mcbloch avatar Nov 17 '20 13:11 mcbloch

Handling wrapping of a line composed of multiple Spans with different styles is going to be a pretty big headache.

Hi @Cldfire, I'm just guessing, but could it be that the Fragment trait introduced in Textwrap 0.13 would help here?

Basically, Textwrap internally wraps such "fragments" which has a width and some trailing whitespace. For plain text, the Word struct is used by Textwrap, but you could imagine a different struct which wraps the styled Spans you talk about.

I recently wrote an example which draws text on a HTML canvas and there I wrap proportional text using a CanvasWord struct.

I would be happy to talk more if you think Textwrap could be useful for tui-rs somehow.

mgeisler avatar Jun 06 '21 20:06 mgeisler

@mgeisler ah that's really neat! It definitely looks like Fragment would be pretty helpful to make wrapping Spans more convenient.

I'm not working on that particular project of mine atm but if I do revisit it at some point in the future I'll definitely try making use of Fragment 👍

Cldfire avatar Jun 06 '21 21:06 Cldfire