flixel/ui/FlxButton.hx
package flixel.ui;
import openfl.events.MouseEvent;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.graphics.atlas.FlxAtlas;
import flixel.graphics.atlas.FlxNode;
import flixel.graphics.frames.FlxTileFrames;
import flixel.input.FlxInput;
import flixel.input.FlxPointer;
import flixel.input.IFlxInput;
import flixel.input.mouse.FlxMouseButton;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.util.FlxDestroyUtil;
#if FLX_TOUCH
import flixel.input.touch.FlxTouch;
#end
enum abstract FlxButtonState(Int) to Int
{
/** The button is not highlighted or pressed */
var NORMAL = 0;
/** The button is selected, usually meaning the mouse is hovering over it */
var HIGHLIGHT = 1;
/** The button is being pressed usually by a mouse */
var PRESSED = 2;
/** The button is not interactible */
var DISABLED = 3;
}
/**
* A simple button class that calls a function when clicked by the mouse.
*/
class FlxButton extends FlxTypedButton<FlxText>
{
/**
* Used with public variable status, means not highlighted or pressed.
*/
@:dox(hide) @:noCompletion
@:deprecated("FlxButton.NORMAL is deprecated, use FlxButtonState.NORMAL")
public static inline var NORMAL = FlxButtonState.NORMAL;
/**
* Used with public variable status, means highlighted (usually from mouse over).
*/
@:dox(hide) @:noCompletion
@:deprecated("FlxButton.HIGHLIGHT is deprecated, use FlxButtonState.HIGHLIGHT")
public static inline var HIGHLIGHT = FlxButtonState.HIGHLIGHT;
/**
* Used with public variable status, means pressed (usually from mouse click).
*/
@:dox(hide) @:noCompletion
@:deprecated("FlxButton.PRESSED is deprecated, use FlxButtonState.PRESSED")
public static inline var PRESSED = FlxButtonState.PRESSED;
/**
* Used with public variable status, means non interactible.
*/
@:dox(hide) @:noCompletion
@:deprecated("FlxButton.DISABLED is deprecated, use FlxButtonState.DISABLED")
public static inline var DISABLED = FlxButtonState.DISABLED;
/**
* Shortcut to setting label.text
*/
public var text(get, set):String;
/**
* Creates a new `FlxButton` object with a gray background
* and a callback function on the UI thread.
*
* @param X The x position of the button.
* @param Y The y position of the button.
* @param Text The text that you want to appear on the button.
* @param OnClick The function to call whenever the button is clicked.
*/
public function new(X:Float = 0, Y:Float = 0, ?Text:String, ?OnClick:Void->Void)
{
super(X, Y, OnClick);
for (point in labelOffsets)
point.set(point.x, point.y + 3);
initLabel(Text);
}
/**
* Updates the size of the text field to match the button.
*/
override function resetHelpers():Void
{
super.resetHelpers();
if (label != null)
{
label.fieldWidth = label.frameWidth = Std.int(width);
label.size = label.size; // Calls set_size(), don't remove!
}
}
inline function initLabel(Text:String):Void
{
if (Text != null)
{
label = new FlxText(x + labelOffsets[FlxButtonState.NORMAL].x, y + labelOffsets[FlxButtonState.NORMAL].y, 80, Text);
label.setFormat(null, 8, 0x333333, "center");
label.alpha = labelAlphas[status];
label.drawFrame(true);
}
}
inline function get_text():String
{
return (label != null) ? label.text : null;
}
inline function set_text(Text:String):String
{
if (label == null)
{
initLabel(Text);
}
else
{
label.text = Text;
}
return Text;
}
}
/**
* A simple button class that calls a function when clicked by the mouse.
*/
#if !display
@:generic
#end
class FlxTypedButton<T:FlxSprite> extends FlxSprite implements IFlxInput
{
/**
* The label that appears on the button. Can be any `FlxSprite`.
*/
public var label(default, set):T;
/**
* What offsets the `label` should have for each status.
*/
public var labelOffsets:Array<FlxPoint> = [FlxPoint.get(), FlxPoint.get(), FlxPoint.get(0, 1), FlxPoint.get()];
/**
* What alpha value the label should have for each status. Default is `[0.8, 1.0, 0.5]`.
* Multiplied with the button's `alpha`.
*/
public var labelAlphas:Array<Float> = [0.8, 1.0, 0.5, 0.3];
/**
* What animation should be played for each status.
* Default is ["normal", "highlight", "pressed"].
*/
public var statusAnimations:Array<String> = ["normal", "highlight", "pressed", "disabled"];
/**
* Whether you can press the button simply by releasing the touch / mouse button over it (default).
* If false, the input has to be pressed while hovering over the button.
*/
public var allowSwiping:Bool = true;
#if FLX_MOUSE
/**
* Which mouse buttons can trigger the button - by default only the left mouse button.
*/
public var mouseButtons:Array<FlxMouseButtonID> = [FlxMouseButtonID.LEFT];
#end
/**
* Maximum distance a pointer can move to still trigger event handlers.
* If it moves beyond this limit, onOut is triggered.
* Defaults to `Math.POSITIVE_INFINITY` (i.e. no limit).
*/
public var maxInputMovement:Float = Math.POSITIVE_INFINITY;
/**
* Shows the current state of the button, either `NORMAL`,
* `HIGHLIGHT` or `PRESSED`.
*/
public var status(default, set):FlxButtonState;
/**
* The properties of this button's `onUp` event (callback function, sound).
*/
public var onUp(default, null):FlxButtonEvent;
/**
* The properties of this button's `onDown` event (callback function, sound).
*/
public var onDown(default, null):FlxButtonEvent;
/**
* The properties of this button's `onOver` event (callback function, sound).
*/
public var onOver(default, null):FlxButtonEvent;
/**
* The properties of this button's `onOut` event (callback function, sound).
*/
public var onOut(default, null):FlxButtonEvent;
public var justReleased(get, never):Bool;
public var released(get, never):Bool;
public var pressed(get, never):Bool;
public var justPressed(get, never):Bool;
/**
* We cast label to a `FlxSprite` for internal operations to avoid Dynamic casts in C++
*/
var _spriteLabel:FlxSprite;
/**
* We don't need an ID here, so let's just use `Int` as the type.
*/
var input:FlxInput<Int>;
/**
* The input currently pressing this button, if none, it's `null`. Needed to check for its release.
*/
var currentInput:IFlxInput;
var lastStatus = -1;
/**
* Creates a new `FlxTypedButton` object with a gray background.
*
* @param X The x position of the button.
* @param Y The y position of the button.
* @param OnClick The function to call whenever the button is clicked.
*/
public function new(X:Float = 0, Y:Float = 0, ?OnClick:Void->Void)
{
super(X, Y);
loadDefaultGraphic();
onUp = new FlxButtonEvent(OnClick);
onDown = new FlxButtonEvent();
onOver = new FlxButtonEvent();
onOut = new FlxButtonEvent();
status = NORMAL;
// Since this is a UI element, the default scrollFactor is (0, 0)
scrollFactor.set();
#if FLX_MOUSE
FlxG.stage.addEventListener(MouseEvent.MOUSE_UP, onUpEventListener);
#end
#if FLX_NO_MOUSE // no need for highlight frame without mouse input
statusAnimations[HIGHLIGHT] = "normal";
labelAlphas[HIGHLIGHT] = 1;
#end
input = new FlxInput(0);
}
override public function graphicLoaded():Void
{
super.graphicLoaded();
setupAnimation("normal", NORMAL);
setupAnimation("highlight", HIGHLIGHT);
setupAnimation("pressed", PRESSED);
setupAnimation("disabled", DISABLED);
}
function loadDefaultGraphic():Void
{
loadGraphic("flixel/images/ui/button.png", true, 80, 20);
}
function setupAnimation(animationName:String, frameIndex:Int):Void
{
// make sure the animation doesn't contain an invalid frame
frameIndex = Std.int(Math.min(frameIndex, animation.numFrames - 1));
animation.add(animationName, [frameIndex]);
}
/**
* Called by the game state when state is changed (if this object belongs to the state)
*/
override public function destroy():Void
{
label = FlxDestroyUtil.destroy(label);
_spriteLabel = null;
onUp = FlxDestroyUtil.destroy(onUp);
onDown = FlxDestroyUtil.destroy(onDown);
onOver = FlxDestroyUtil.destroy(onOver);
onOut = FlxDestroyUtil.destroy(onOut);
labelOffsets = FlxDestroyUtil.putArray(labelOffsets);
labelAlphas = null;
currentInput = null;
input = null;
#if FLX_MOUSE
FlxG.stage.removeEventListener(MouseEvent.MOUSE_UP, onUpEventListener);
#end
super.destroy();
}
/**
* Called by the game loop automatically, handles mouseover and click detection.
*/
override public function update(elapsed:Float):Void
{
super.update(elapsed);
if (visible)
{
// Update the button, but only if at least either mouse or touches are enabled
#if FLX_POINTER_INPUT
updateButton();
#end
// Trigger the animation only if the button's input status changes.
if (lastStatus != status)
{
updateStatusAnimation();
lastStatus = status;
}
}
input.update();
}
function updateStatusAnimation():Void
{
animation.play(statusAnimations[status]);
}
/**
* Just draws the button graphic and text label to the screen.
*/
override public function draw():Void
{
super.draw();
if (_spriteLabel != null && _spriteLabel.visible)
{
_spriteLabel.cameras = _cameras;
_spriteLabel.draw();
}
}
#if FLX_DEBUG
/**
* Helper function to draw the debug graphic for the label as well.
*/
override public function drawDebug():Void
{
super.drawDebug();
if (_spriteLabel != null)
{
_spriteLabel.drawDebug();
}
}
#end
/**
* Stamps button's graphic and label onto specified atlas object and loads graphic from this atlas.
* This method assumes that you're using whole image for button's graphic and image has no spaces between frames.
* And it assumes that label is a single frame sprite.
*
* @param atlas Atlas to stamp graphic to.
* @return Whether the button's graphic and label's graphic were stamped on the atlas successfully.
*/
public function stampOnAtlas(atlas:FlxAtlas):Bool
{
var buttonNode:FlxNode = atlas.addNode(graphic.bitmap, graphic.key);
var result:Bool = (buttonNode != null);
if (buttonNode != null)
{
var buttonFrames:FlxTileFrames = cast frames;
var tileSize:FlxPoint = FlxPoint.get(buttonFrames.tileSize.x, buttonFrames.tileSize.y);
var tileFrames:FlxTileFrames = buttonNode.getTileFrames(tileSize);
this.frames = tileFrames;
}
if (result && label != null)
{
var labelNode:FlxNode = atlas.addNode(label.graphic.bitmap, label.graphic.key);
result = result && (labelNode != null);
if (labelNode != null)
label.frames = labelNode.getImageFrame();
}
return result;
}
/**
* Basic button update logic - searches for overlaps with touches and
* the mouse cursor and calls `updateStatus()`.
*/
function updateButton():Void
{
// Prevent interactions with this input if it's currently disabled
if (status == DISABLED)
return;
// We're looking for any touch / mouse overlaps with this button
var overlapFound = checkMouseOverlap();
if (!overlapFound)
overlapFound = checkTouchOverlap();
if (currentInput != null && currentInput.justReleased && overlapFound)
{
onUpHandler();
}
if (status != NORMAL && (!overlapFound || (currentInput != null && currentInput.justReleased)))
{
onOutHandler();
}
}
function checkMouseOverlap():Bool
{
var overlap = false;
#if FLX_MOUSE
for (camera in getCameras())
{
for (buttonID in mouseButtons)
{
var button = FlxMouseButton.getByID(buttonID);
if (button != null && checkInput(FlxG.mouse, button, button.justPressedPosition, camera))
{
overlap = true;
}
}
}
#end
return overlap;
}
function checkTouchOverlap():Bool
{
var overlap = false;
#if FLX_TOUCH
for (camera in getCameras())
{
for (touch in FlxG.touches.list)
{
if (checkInput(touch, touch, touch.justPressedPosition, camera))
{
overlap = true;
}
}
}
#end
return overlap;
}
function checkInput(pointer:FlxPointer, input:IFlxInput, justPressedPosition:FlxPoint, camera:FlxCamera):Bool
{
if (maxInputMovement != Math.POSITIVE_INFINITY
&& justPressedPosition.distanceTo(pointer.getScreenPosition(FlxPoint.weak())) > maxInputMovement
&& input == currentInput)
{
currentInput = null;
}
else if (overlapsPoint(pointer.getWorldPosition(camera, _point), true, camera))
{
updateStatus(input);
return true;
}
return false;
}
/**
* Updates the button status by calling the respective event handler function.
*/
function updateStatus(input:IFlxInput):Void
{
if (input.justPressed)
{
currentInput = input;
onDownHandler();
}
else if (status == NORMAL)
{
// Allow "swiping" to press a button (dragging it over the button while pressed)
if (allowSwiping && input.pressed)
{
onDownHandler();
}
else
{
onOverHandler();
}
}
}
function updateLabelPosition()
{
if (_spriteLabel != null) // Label positioning
{
_spriteLabel.x = (pixelPerfectPosition ? Math.floor(x) : x) + labelOffsets[status].x;
_spriteLabel.y = (pixelPerfectPosition ? Math.floor(y) : y) + labelOffsets[status].y;
}
}
function updateLabelAlpha()
{
if (_spriteLabel != null && labelAlphas.length > (status : Int))
{
_spriteLabel.alpha = alpha * labelAlphas[status];
}
}
/**
* Using an event listener is necessary for security reasons on flash -
* certain things like opening a new window are only allowed when they are user-initiated.
*/
#if FLX_MOUSE
function onUpEventListener(_):Void
{
if (visible && exists && active && status == PRESSED)
{
onUpHandler();
}
}
#end
/**
* Internal function that handles the onUp event.
*/
function onUpHandler():Void
{
status = HIGHLIGHT;
input.release();
currentInput = null;
// Order matters here, because onUp.fire() could cause a state change and destroy this object.
onUp.fire();
}
/**
* Internal function that handles the onDown event.
*/
function onDownHandler():Void
{
status = PRESSED;
input.press();
// Order matters here, because onDown.fire() could cause a state change and destroy this object.
onDown.fire();
}
/**
* Internal function that handles the onOver event.
*/
function onOverHandler():Void
{
#if FLX_MOUSE
// If mouse input is not enabled, this button must ignore over actions
// by remaining in the normal state (until mouse input is re-enabled).
if (!FlxG.mouse.enabled)
{
status = NORMAL;
return;
}
#end
status = HIGHLIGHT;
// Order matters here, because onOver.fire() could cause a state change and destroy this object.
onOver.fire();
}
/**
* Internal function that handles the onOut event.
*/
function onOutHandler():Void
{
status = NORMAL;
input.release();
// Order matters here, because onOut.fire() could cause a state change and destroy this object.
onOut.fire();
}
function set_label(Value:T):T
{
if (Value != null)
{
// use the same FlxPoint object for both
Value.scrollFactor.put();
Value.scrollFactor = scrollFactor;
}
label = Value;
_spriteLabel = label;
updateLabelPosition();
return Value;
}
function set_status(value:FlxButtonState):FlxButtonState
{
status = value;
updateLabelAlpha();
return status;
}
override function set_alpha(Value:Float):Float
{
super.set_alpha(Value);
updateLabelAlpha();
return alpha;
}
override function set_x(Value:Float):Float
{
super.set_x(Value);
updateLabelPosition();
return x;
}
override function set_y(Value:Float):Float
{
super.set_y(Value);
updateLabelPosition();
return y;
}
inline function get_justReleased():Bool
{
return input.justReleased;
}
inline function get_released():Bool
{
return input.released;
}
inline function get_pressed():Bool
{
return input.pressed;
}
inline function get_justPressed():Bool
{
return input.justPressed;
}
}
/**
* Helper function for `FlxButton` which handles its events.
*/
private class FlxButtonEvent implements IFlxDestroyable
{
/**
* The callback function to call when this even fires.
*/
public var callback:Void->Void;
#if FLX_SOUND_SYSTEM
/**
* The sound to play when this event fires.
*/
public var sound:FlxSound;
#end
/**
* @param Callback The callback function to call when this even fires.
* @param sound The sound to play when this event fires.
*/
public function new(?Callback:Void->Void, ?sound:FlxSound)
{
callback = Callback;
#if FLX_SOUND_SYSTEM
this.sound = sound;
#end
}
/**
* Cleans up memory.
*/
public inline function destroy():Void
{
callback = null;
#if FLX_SOUND_SYSTEM
sound = FlxDestroyUtil.destroy(sound);
#end
}
/**
* Fires this event (calls the callback and plays the sound)
*/
public inline function fire():Void
{
if (callback != null)
callback();
#if FLX_SOUND_SYSTEM
if (sound != null)
sound.play(true);
#end
}
}