Custom Scroll Bar Logic

I’m currently doing a spike on setting up custom scrolling logic in Elm.

The idea is that if you have a large file to display, perhaps a log or a large file in an editor, rendering the full view for it each time, can be slow. The default way of using overflow: scroll on the div works, but is not optimal. Instead scroll bars are set up in separate divs to the one showing the content, their scroll events are hooked up, and custom logic will take a slice of the content and render a smaller view just for what is visible.

Has anyone done this before?

I’m taking some inspiration from CodeMirror - handy at least to be able to see how this is done in Javascript, and what the CSS used there is.

Demo and Code

The current WIP is here:

demo: https://mystifying-gates-6761c1.netlify.app/

code: https://github.com/rupertlssmith/elm-scroll-spike/blob/main/src/elm/Main.elm

Going to be looking at:

  • Calculating the size of the div to scroll, so scroll bars can be appropriately sized.
  • Implementing the slice, and positioning the lines to be displayed.
  • Mouse wheel, page up, page down, ctrl+home, ctrl+end and so on.
  • Listening for resize events and re-doing the size calcs.
  • Optimization so the scrolling is as smooth as I can get it, and update/view cycle is quick enough to support an editor on large files.
1 Like

I had done a similar thing, scrolling a list with 100000 entries. It works, as long the displayed lines or entries have the same height. I used an idea, I found somewhere on github, implemented in jquery, but I can not find the original source anymore.

It works like that:

Each text-line is a div. Calculate, how may pixel this div needs vertical, e.g. by drawing a test-div.
Create a scrollable div, the scene (https://package.elm-lang.org/packages/elm/browser/latest/Browser.Dom). In this scene display the divs containing lines.

Now the optimization:
Merge all divs with lines before the viewport to one big div, without any content.
Similar merge for all divs after the viewport.

So only the viewable divs with lines are rendered.

For better user-experience with small scrolls add some divs with content before and after the viewport (and decrease the big divs properly)

That sounds like a good idea and has the advantage that you just use overflow: scroll on the containing div, no need for putting scroll bars in separate divs. Thanks.

Thinking about it, do I even need the big divs before and after the viewport? I was going to use “position: absolute” on each line div anyway. So if I just position the lines correctly within the overall container div, and then scroll that? I’ll give it a go anyway.

You might want to look up the term “virtual scrolling”. It might give you some more resources on how others are doing this.

1 Like

Thanks for that tip. I was searching for “custom scrolling” which only turns up hits for how to customize the look of scroll bars with CSS or other techniques - was thinking it is hard to search for what I am after as a result. :+1:

1 Like

Thinking about it, do I even need the big divs before and after the viewport?

The intention of the big divs is to have a proper sized div around, with a proper sized scrollbar. Of course you can also use a calculated height.

1 Like

One current package that has the same aim, I think, is https://package.elm-lang.org/packages/FabienHenon/elm-infinite-list-view/latest/

So I’ve done all that and updated the demo, seems to work pretty nicely:

https://mystifying-gates-6761c1.netlify.app/

I made use of Html.Lazy and Html.Keyed and I could see that made a small but barely discernable difference in performance.

The only other thing I would like to do is to override the arrow key and page up/down events, so that they always scroll by an exact integer number of lines. When going down, the last visible line of text should appear whole at the bottom, when going up the first visible line should appear whole at the top.

Plan is to use this for a code editor along these lines: Text editor done in pure Elm.

1 Like

We have an infinite sc[quote=“rupert, post:9, topic:6403”]
The only other thing I would like to do is to override the arrow key and page up/down events, so that they always scroll by an exact integer number of lines.
[/quote]

If you are writing a text editor I might be worth capturing all keyboard events to have full control of what is going on, specially if it is going to be an advanced text editor. Check elmMPS, for instance; I think that it would be impossible to create it without taking full control of use input.

I’m thinking the hidden textarea approach used by CodeMirror will really be necessary - that will handle most of the keyboard work.

https://codemirror.net/doc/internals.html

Anyway, for this spike I’ll just handle a few keys on the div and call it quits. Going to look at implementing an efficient and flexible content buffer next, since I got an idea for that.

Nice! Thanks for sharing the link.

Implemented this too. Now arrow keys or page up/down always move in increments modulo the line height with the top or bottom line always holding a full line, depending on which direction you are scrolling towards. That really helped make the scrolling look smooth. I also found that rendering above and below 1 full page of lines removed any flicker on page up and down:

https://mystifying-gates-6761c1.netlify.app/

Small bug: Pressing PgUp some times after load causes PgDown not working, until the same number of keypresses is done on both keys. Maybe some underflow?

It is also interesting to see, how the memory-usage increases while scrolling, until the GC started, but not to critical value, I suppose.

Yes, it does that. Didn’t seem critical to fix it, as I was more interested in the scrolling.

I think @Janiczek and @kraklin have something similar in app the’re working on at work. just in that case it’s not a text but a table and it also performs network requests per cells with scrolling both on x and y axis.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.