Saturday, January 30, 2016

Scrollbars usable!

They're not complete yet, but they're quite usable for my use case now, as of this commit.  The other features will come later, unless of course you'd like to...

Fork me on GitHub
You're welcome, Reddit person. If that's not what you were looking for, I hope you find it somewhere (or make it) and share it :)

Huzzah! Scrollbars, drawn.

It's a bit unfortunate, but I had to scrap the idea of using FlxCamera.zoom because the calculations got too messy for me.  So instead I'm back to the original, which is that you lay out everything manually yourself.

What did I do about the merry-go-round?  Your layout code works with the scroll area code.  You tell it whether you intend to FIT_WIDTH or FIT_HEIGHT, and then it tells you whether you can expect a scrollbar, or, in the weird glitchy zone of the merry-go-round, whether to do the opposite of what you normally would, so that it's a bit less glitchy.

There are some off-by-one conversion areas which make the glitchy zone still a tiny bit glitchy, but overall I'm fairly happy with the result.  This is a spoiler (if not much of one) from a game whose name is still under wraps, but you'll hear about more soon.  These show FIT_HEIGHT, although probably you would typically want FIT_WIDTH, which is more like a web page with a vertical scroll bar.

Normal window

Getting too short (you can see the scrollbar almost running out; the "CC" trophy is the last one)

 Too short for a scrollbar, but too tall to max the height of everything (this would be where the merry-go-round would normally happen)
Short and wide enough that we can max the height of everything without a scrollbar
The code is not yet complete (only the drawing is done, not the input response) but it's available here:

Input response will be next.  For now, you can plunk those .hx files in your own project's source/flixel/addons subdirectory and create a FlxScrollableArea in your state's create() function.  This was made and tested with a non-standard scale mode in mind, where you want full control over resizing your content, so the next step is to override onResize() in your state, and there set the .viewPort property of your FlxScrollableArea, re-layout your content based on its .bestMode/horizontalScrollbarHeight/verticalScrollbarWidth properties, then finally set the .content property to match your newly-laid-out content.

Scrollbar Merry-Go-Round

I didn't have too much time yesterday to work on this, but what I did manage to get done was the drawing code, so I now have a class that draws its own scrollbars correctly.

Meanwhile, I noticed that my state class was having to guess whether to leave space for scrollbars or not.  I opened up the scrollbar class to make this a gettable property, which helped, but there was a still a snag...

The Scrollbar Merry-Go-Round...of doom

I guess I'm not surprised to see this in my own basic code, because I've seen this for years, and still do, in various (otherwise professional) web pages.  Not often (ever?) in a scrolling context, but in a CSS :hover selector.  Once triggered, the merry-go-round functions automatically without moving the mouse pointer.  What happens is this:

  1. You mouse over an element, near its edge.
  2. The :hover effect causes a change in proportions in the element (e.g., the text goes bold, so the element has to get wider, or rewraps and gets taller, etc.).
  3. Because the element's edges move, it is now is no longer under the mouse pointer.
  4. The :hover selector no longer applies.
  5. The element returns to its original dimensions.
  6. The mouse pointer is suddenly hovering over it again, like it was in the first step, because we haven't moved it.
  7. The browser processes the above steps repeatedly in the most jittery, jarring rapid succession.
It's perhaps not clear how to solve such a problem at the CSS level, other than to make your selector style not change the size of the element in any way.  The only other option would be a bunch of JavaScript or browser hacking so that, until the mouse pointer actually moves, you cannot return to a non-hovering state.  (I'm sure that fixing this at the browser level would break lots of pages, e.g. games that rely on the hover effect even when the mouse cursor is not moving, because elements might be moving on their own.)

Unfortunately, in my case, neither of these is the answer that I'm looking for.

In my case, I have some content that I would like to take up the full height of the window, unless the content does not fit the width, in which case I would like the content to take up the full height minus the scrollbar thickness.  Sounds simple enough.  However, in practice, there is a set of window sizes that lead to the Scrollbar Merry-Go-Round.  It's not quite as automatic as the CSS case above, but it's visually at least as bad, because of how big scrollbars normally are.

If you're simplistic enough in the logic in the state class, and just have something like this in onResize:

content.setGraphicSize( 0, Lib.current.stage.stageHeight - (_scrollable.horizontalScrollbarIsVisible?_scrollbarThickness:0) );

...then you will find that, depending on whether you are resizing up or down, and/or the results of the last resize, your visual result will alternate between having scrollbars and not having them, and in both cases not look very good.

This is because we're basing our new content size on whether scrollbars are needed for the current content size.

Even if you try to make it slightly smarter (if that much more inefficient) and call onResize again if and only if the need for scrollbars changed after you resized the content, it's at least as visually quirky.

So what can be done?  On a small display, the first solution for the CSS problem (don't change the size) means that I will have a scrollbar's thickness taking up a chunk of the window whether I need it to or not (and this gets worse when the scrollable area is not the whole window.)  The second solution doesn't apply because unlike the CSS merry-go-round, my scrollbar problem requires mouse movement to show its full glitchy glory.

I had written this comment detailing my thoughts, which are hopefully possible to implement:

The parent state shouldn't have to worry about whether or not a scrollbar is showing, or even about resizing content.  After all, we have cameras and they can zoom.

We have to calculate the content's ratio and find the point at which it will fit with unneeded scrollbars (or each one independently?  even more complex...).  We also need the point at which it will fit without scrollbars.  In between these two points is the merry-go-round, and instead of this, we should just take away the scrollbars (or a given scrollbar?) and use up part of its space, but not enough to require scrollbars again.

Time to figure out some calculations...

Although, this solution seems to assume my particular context, that I want the content to take up as much vertical space as possible, and no vertical scrollbar should ever show.  Probably a more normal context is the other way around.  And then there's a third context, where the content would not change, and you might have any combination of scrollbars but no merry-go-round.  Perhaps the camera subclass can have these three as options.

Thursday, January 28, 2016

On the Road to HaxeFlixel Scrollbars

I found out that scrolling how I would like it is fairly feasible using a FlxCamera derivative.  If I just update .scroll (rather than calling .setPosition() as I had initially thought) I can control the camera how I envisioned in this class. It so far doesn't do much beyond a basic FlxCamera, but will hopefully soon have scrollbar-drawing and mouse-handling code.
package flixel.ui;

import flixel.FlxCamera;
import flixel.math.FlxRect;

* An area of the screen that has automatic scrollbars, if needed.
* @author Gimmicky Apps
class FlxScrollableArea extends FlxCamera
* Creates a specialized FlxCamera that can be added to FlxG.cameras.
* @param ViewPort the area on the screen, in absolute pixels, that will show content.
* @param Content the area (probably off-screen) to be viewed in ViewPort
public function new(ViewPort:FlxRect, Content:FlxRect)
super( ViewPort.x ), ViewPort.y ), ViewPort.width ), ViewPort.height ),
1 );
_bounds = Content;
scroll.x = Content.x;
scroll.y = Content.y;
I was thinking of trying FlxSliders for the scrollbars, but visually I don't think they fit well. That's OK, a basic scrollbar will not be hard to draw. :)

The Quest for a HaxeFlixel Scrollable Area

Something tells me this is a wheel I probably shouldn't be re-inventing.  However, I don't see many alternatives.

The wheel to which I refer?  A scrollbar.  (Or swipe-ability in the case of touch.)  In HaxeFlixel.  Google it in the past year.  See?  It's pretty much just this one person lately asking for it on Reddit.

But they're not the only one over the years, just the most recent.

You might find this thread and Joshua Granick's post which refer to his Scrollbar.hx and seem promising, but it was made with NME/Flash in mind.  It's not clear whether this would work well with HaxeFlixel, targeting other platforms like Android.  It's certainly not made with the same model in mind, using timers in place of an overridden update() function.  If it would work, it would need forking and one could probably replace Utils.constrain with FlxMath.bound, and use HF's own tweening library.

You might find this flixel-ui issue showing more interest, but also a dead-end in terms of finding something ready-made, even if you're wanting something within the flixel-ui world instead of vanilla HaxeFlixel.  Later this question was posted, the end result being a suggestion of a code bounty to move things along.  (Although, Gama11 also linked to this code, which had been viewed 193 times as of this writing.  Of the things I found this seems the furthest along, but it's now outdated, and at that point it was also incomplete.)

I'm wondering if it's time to simply extend FlxCamera with a new class, FlxUserScrolledCamera, barring a name having more finesse coming to mind.  The idea would be, place all your content off-screen, wherever is most convenient.  Then, create a new FlxUserScrolledCamera, whose bounds are around your off-screen content, and voila--either the user can swipe at it to scroll around, or use the scroll bars (which auto-hide), depending on the platform.  Optionally they could zoom with pinch gestures or a mousewheel.  The scroll bars themselves, if any, would be drawn somewhere on-screen, to be viewed not by the camera itself but by the main camera.

To me this represents the simplest solution, but maybe it's too good to be true.  If not, I might as well just do it and share the results!

Beware the Invalid BitmapData

Just a note to fellow HaxeFlixel developers, in case it saves you some time.

If you extend a FlxTypedGroup<FlxSprite>, don't instantiate a variable of its kind in a class definition, outside a function.  I.e. don't do this:

class XYZ {
public var myFtgInstance = new extendedFtg();

Instead, move the myFtgInstance = new extendedFtg(); into your new() or create() or whatever function is applicable.

The context for my story is that I did not at first realize there were FlxSubStates, so I rolled my own Popup class (extending FlxTypedGroup<FlxSprite>).  In the end, I sort of liked how mine was organized better, anyway.  I extended Popup into a few different specialized Popups, and they worked fine.

A few weeks later I went to make a new specialized Popup for a different state, and got a very hard-to-track-down runtime error, Invalid BitmapData, whose stack trace did not even originate in my project's code.

In the end, it was only because my state class code foolishly tried to instantiate the Popup derivative outside any function.  Unfortunately, figuring this out was not straightforward at all, in the end being fixed only thanks to an inspired guess.