2008-01-22

Scrollbar Confusion

Scrollbars are still in progress. I hit a couple of snags along the way, one of which I’ve solved, and one which I haven’t.

The first problem was implementing the “maximum” and “minimum” values that Jeff suggested. The idea is that the developer passes two values to the scrollbar - maximum and minimum values - and the scrollbar automatically works out (based on its own height) how large its grip should be and the value of each pixel within the gutter. Using a simple ratio calculation we can work all of this out.

This worked fine when the scrollbar is thought of as a slider, but it falls apart when the scrollbar is, well, a scrollbar. In the first scenario, the grip always has a value of “1”. So, if we give it a range of values between 0 and 9, and make the scrollbar 100 pixels high, the grip will make itself 10 pixels high. Dragging the grip increases or decreases the value of the scrollbar as a whole by 1.

Imagine that this scrollbar is attached to a textbox that is 9 pixels high. Now we have a problem - the scrollbar will resize itself and suggest that the textbox contains 10 screens of text, but in fact there’s one screen of text and an additional row of pixels that can be scrolled into view.

This had me stumped for a while until I realised that I needed to pass into the scrollbar the height of the viewport into the scrollable area. At that point the calculations all made sense.

Duh.

The scrollbar can still be used as a slider simply by setting the viewport size (currently unhelpfully called the “page size”) to 1.

The second snag is more tricky. When I wrote the ScrollablePanel gadget, I implemented the minimum and maximum scrollable values as the limits within which an area could be scrolled. So, if you have a ScrollablePanel with a minimum value of 0 and a maximum value of 1, that panel can be scrolled by 1 pixel.

The MultiLineTextBox and Scrollbar gadgets work differently, though. The textbox provides a viewport into a larger scrollable region with its own width and height. The region should not scroll unless its height is larger than the surrounding viewport, so the scrolling values have to be adjusted to compensate for the height of the viewport. Unfortunately, to make the Scrollbar more generally useful, that also has to take into account the height of the viewport.

This leaves me with a host of problems:

  • The ScrollablePanel and MultiLineTextBox using two different and incompatible models for their scrollable regions
  • The ScrollablePanel and Scrollbar are also using incompatible models
  • The workarounds implemented in the MultiLineTextBox break the correct functionality implemented in the Scrollbar.

I need to change the ScrollablePanel to match the MultiLineTextBox, then strip out the workarounds, and everything should work.

Comments

Jeff on 2008-01-27 at 07:00 said:

I’ve been away for a week, sorry not to have replied sooner.

The notion of “proportional” scrollbars where the size of the GRIP is dynamic is something you can’t do with the simple minimum/maximum values being passed in, as you note. But I don’t think you should be passing in viewport heights - you want to tell the scrollbar the ratio of “document height / viewport height” and it would display its grip using the same ratio.

Normal scrollbars could guess the viewport height since its is most likely the same as their own, but they can’t determine what proportion of the document is being displayed. It may be easier to pass the two values in as integers; ie, pass a ratio, rather than a float.

Addressing the case where the viewport shows the entire document, I think the key is that min and max values should be IDENTICAL, in which case the scrollbar should know to blank itself out. In the case of a scrolling bitmap, the min and max values would be 0 and (document_height - viewport_height). Um, +1 ? Whatever, I always get that stuff wrong. But essentially, the scrollbar is working through the LINE OFFSET of the TOP LINE in the bitmap. The top line wouldn’t be the number of pixels in the document unless you want to scroll it completely off the top.

(In fact, the scrollbar should disable itself if min>=max to make things simpler if you try to draw a bitmap that is SMALLER than the viewport - less special case math required in the bitmap gadget)

ant on 2008-01-27 at 12:52 said:

The ratio is a good plan, but I think it adds a level of complexity above just passing in the viewport height and allowing the scrollbar to figure the ratio out for itself. I’m not sure if the DS supports floating-point values. I know that the Cybiko didn’t, but then the DS is considerably more complex so it might. However, I’m avoiding floats because of the extra CPU time they require. At the moment, the scrollbar bitshifts the ratio calculations up a byte to simulate fixed-point fractions.

Yeah, I’ve got the min == max stuff in there, and the scrollbar caters for min > max too.

Just need to keep tinkering with it until it works, both in terms of functionality and with regards to the API.

Jeff on 2008-01-27 at 20:55 said:

I think you need to explain how “viewport height” is sufficient, or at least distinguish it from actual pixel counts.

I’m assuming that the SCROLLABLE window contains a VIEWPORT (lets call it 200 pixels tall) which displays a bitmap DOCUMENT that is 1000 pixels tall. In this case, the SCROLLBAR grip should be 200 / 1000 = one fifth of the available space.

The only way to compute this is if you have the DOCUMENT height and the VIEWPORT height.

Now, if you are saying that the “viewport height” is actually a number taken from the same units as “min” and “max” then I guess thats fine, I think. It gets sticky for my List gadget which allows different-height rows; the number of rows that are displayed in the viewport will change depending on the actual rows selected, but the min and max row numbers will not vary. The grip size would vary as it moved up/down the list - clearly bad user interface. Thus, in this case a proportional sized grip is inappropropriate.

ant on 2008-01-27 at 23:39 said:

The scrollbar currently works out the document height from the min and max values (max - min = doc height). From here, we can work out the ratio of document height to scrollbar height (doc height / scrollbar height = ratio). Passing in the viewport height gives us the height of the grip (viewport height / ratio).

At this point, we can work out the “value” of each pixel in the scrollbar gutter, which will fall between min and max (formula in the code somewhere; VM’s not fired up at the mo). We can read out the value by getting the position of the grip and passing it through the same formula, and we can set the value by repositioning the grip (passed through the reverse of the same formula).

This gives us everything we need - height of the grip, value of the grip’s position, and the maximum and minimum values that the grip can represent.

For the list gadget. you’d send in 0 as the minimum value and the number of list items as the maximum value. Pass in 1 as the viewport size. The grip will resize to represent a single item (we ignore the actual pixel height of an item - we’re not interested in that). When the user drags the grip down, the scrollbar will fire a value changed event when the grip has moved enough to represent a whole list item. Getting the value of the scrollbar will return the item number, not the pixel position, that should be shown at the top of the list. It’s up to the list to redraw itself at the right location.

The scrollbar can be used to scroll around any kind of numerical data. It works with pixel values, but if you want to use some other unit, the scrollbar will work with that, too - you just have to interpret the values you put in and get out.

Of course, you run into problems if your list items are larger than the viewport. In that case, you’d have to work in pixels instead. Not the best solution as you wouldn’t be jumping exactly from one list item to the next.

Jeff on 2008-01-28 at 04:41 said:

I was going to say this:

Sorry, its still not clear to me which viewport you are talking about. If its the viewport that the scrollbar lives in, then the scrollbar already knows it. If its the viewport that the document is being drawn in, then its not guaranteed to be the same size as the scrollbar, and thus you can’t compute the grip size from it.

but I finally realised that we are talking about two distinct ratios. You are working from the ratio of “document height to scrollbar height” whereas I’m working from “document-to-viewport”.

The thing about my approach is that the “document-to-viewport” height is pretty much always going to be expressible as two integers in the same coordinate range - for a bitmap, I divide pixels by pixels, for a list, I do lines by lines. Your approach is going to dive lines by pixels, then compensate later by multiplying pixels by (lines/pixels) - in the DS world, thats probably feasible, I’m used to worrying about converting real-world (lat/long) to screen (x/y) and the errors that can entail.

I think the thing about passing ‘1’ as viewport size is that its a hack - it deliberately misleads the caller, achieving a commonly desired result through trickery, whereas if you passed in the ratio between view and document and told the scrollbar to use that as ‘grip size’, its clear (from the outside) what is being requested. Clearly this is a subjective opinion.

I assume you mean that the pixel position is the grips CENTRE, not its TOP, so the available number of pixel positions is (scrollbar.height - grip.height) assuming that you don’t put up/down arrows at the ends (as part of the scrollbar)

Its important to remember that because you are doing integer math (with pixel positions), you have non-reversible math. ie, there may be MULTIPLE pixel positions in the scrollbar that correspond to the same scroll value, if the range of values in the document is small. This changes the behaviour of the grip - you can either allow partial-drags which affect the graphics, but don’t affect the SCROLLABLE, or you snap the grip to discrete positions, which makes it jerky if you do it during the drag, or snappy if you do it after.