Using ‘Image’ rather than ‘Button’ in ScriptUI.

The starting point is very simple: instead of using the native ScriptUI Button wrapper, we want to display a pure Image object that will receive mouse events and behave like a button.

Note. — The Image widget is poorly documented and should not be confused with the ScriptUIImage structure that encapsulates a few properties of an actual image. For its part, the Image widget is a simple container and it virtually operates as a Group. Although it may remain empty it is designed to contain a ScriptUIImage that is accessed to by the image (or icon) property.

Since ScriptUI 4.0, every component —including passive widgets such as Group and Image— is allowed to dispatch mouseover, mousedown, mouseout, and mousemove events. Hence we can write event listeners to make this kind of widget more reactive. Of course it is possible —though not so obvious!— to dynamically change the underlying bitmap of an Image object. But why not trying to break the back of the beast with a single image?

Three Sprites under the hood

To mimic a reactive button, what we basically need is a three-state component, as shown below:

Three states of the image-based button.

An important prerequisite is that the different states of this triptych have the same dimensions. Now let's see the situation from a ‘sprite’ perspective:

The sprite perspective.

The above screen shows the sliced PNG image (1), and the state of the GUI (2) when the mouse rolls over the widget. All we have to do is to appropriately shift the image along the vertical axis of that container. The container acts like a mask: it displays the relevant part of the bitmap within its own area.

Technically, the whole trick is based on the ScriptUIGraphics.drawImage(…) method, defined as follows:

    drawImage(image, x, y, width, height)

where:

image refers to a ScriptUIImage object;
x (Number) is the (positive or negative) left coordinate of the region, relative to the origin of the element (0 is default);
y (Number) is the (positive or negative) top coordinate of the region, relative to the origin of the element (0 is default);
width and height (Number) are the dimensions of the image. If provided, the image is stretched or shrunk to fit. If omitted, uses the original image dimensions.

As you probably guessed, we are going to play with the y argument…

Further in the code

There are two critical issues to be aware of:

• By default, an Image widget —i.e. the container— will get the size of the underlying bitmap at construction time, provided that we set the content at construction time and that we do not override the default mechanism:
    myImage = myWindow.add('image', undefined, myBitmap).
Note that myBitmap might be: a File (PNG, JPEG), a ‘resource’ code (see Peter Kahrel's ScriptUI for Dummies for advanced details on this topic), a ScriptUIImage, or even a String that directly contains the bytes of the bitmap —undocumented but really cool feature!
Then, in order to use the sprite-based strategy, we have to explicitly set the size of the container by dividing by three the height of the image. As I wanted to keep my code as generic as possible, I preferred not to hard-code the size of any component. Therefore I let the default mechanism detects the actual size of the supplied bitmap, then I refine the size of the container:

// . . .
 
// Number of vertical slices
// ---
var V_SPRITES = 3;
 
// Create the UI
// ---
var w = new Window('dialog',"ScriptUI Sprites"),
    myImage = w.add('image', undefined, myPNG),
    iSize = myImage.image.size,
    spriteHeight = iSize[1] / V_SPRITES;
 
myImage.size = [iSize[0], spriteHeight];
 
// . . .
 

• Another important point is that we cannot invoke myImage.graphics.drawImage(...) without precaution. As far as I know, such operation is only available during the draw event, hence in the scope of an onDraw routine. That's why the mouse event handler artificially forces a redraw via the Image's parent LayoutManager:
    myImage.parent.layout.layout(true);
(To be honest I do not like this solution, so feel free to suggest a better one!)

Note. — I also observed that when the image is shifted within its container by a non-zero offset, we need to compensate the move for 1 pixel! I failed to explain why. Maybe it's a bug…

Finally, here is the main routine of the script:

// . . .
 
myImage.onDraw = function()
{
    var dy = this.properties.state*spriteHeight;
    if( dy ) ++dy;  // patch ScriptUI offset bug
    this.graphics.drawImage(this.image,0,-dy);
};
 
var mouseEventHandler = function(ev)
{
    // Update the 'state' of myImage (internal flag)
    // ---
    this.properties.state = ('mouseover'==ev.type)+
        2*('mousedown'==ev.type);
 
    // Force onDraw
    this.parent.layout.layout(true);
};
 
// Register the mouse event handler
// ---
myImage.addEventListener('mouseover', mouseEventHandler);
myImage.addEventListener('mousedown', mouseEventHandler);
myImage.addEventListener('mouseup', mouseEventHandler);
myImage.addEventListener('mouseout', mouseEventHandler);
 
// . . .
 

I hope you'll enjoy customizing ScriptUI buttons!