Difference between revisions of "ButtonMonster class"

From GiderosMobile
 
(8 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
__TOC__
 
__TOC__
  
This button class is useful to build buttons which you can navigate using the mouse and the keyboard. It is optimised for menus and in game.
+
This ButtonMonster class allows you to build any button, from the simplest (text) to the most complicated (tooltip, keyboard navigation). It is optimised for menus and in games.
  
'''Note: needs Gideros ''luashader'' Library'''
+
'''[[Media:buttonMonster.lua]]''' '''(tip: right click and save link as)'''
  
 
=== ButtonMonster '''Class''' ===
 
=== ButtonMonster '''Class''' ===
Line 9: Line 9:
 
--[[
 
--[[
 
-- ButtonMonster
 
-- ButtonMonster
 +
-- mokalux, cc0
 
-- Pixel, Image, 9patch, Text, Tooltip,
 
-- Pixel, Image, 9patch, Text, Tooltip,
 
-- Up, Down, Disabled, Hover,
 
-- Up, Down, Disabled, Hover,
 
-- Sfx, Touch, Mouse and Keyboard navigation!
 
-- Sfx, Touch, Mouse and Keyboard navigation!
v 0.2.0: 2023-11-29 terminator, should be fine in games too now
+
-- Functions
 +
v 0.2.2: 2024-11-08 selectionSfx(): added channel checks before setting volume
 +
v 0.2.1: 2024-04-22 added function callback (functions with no parameters for now!)
 +
v 0.2.0: 2023-12-01 terminator, should be fine in games too now
 
v 0.1.0: 2021-06-01 total recall, this class has become a Monster! best used in menus but who knows?
 
v 0.1.0: 2021-06-01 total recall, this class has become a Monster! best used in menus but who knows?
 
v 0.0.1: 2020-03-28 init (based on the initial generic Gideros Button class)
 
v 0.0.1: 2020-03-28 init (based on the initial generic Gideros Button class)
 
]]
 
]]
 
-- Shader, please adapt the path accordingly!
 
--!NEEDS:../luashader/luashader.lua
 
 
function vshaderpixelslot(vVertex, vColor, vTexCoord) : Shader
 
local vertex = hF4(vVertex, 0.0, 1.0)
 
fTexCoord = vTexCoord
 
return vMatrix*vertex
 
end
 
function fshaderpixelslot() : Shader
 
local frag = texture2D(fTexture, fTexCoord) -- 0, not focused
 
if slot == 2.0 then frag = texture2D(fTexture3, fTexCoord) -- 2, disabled
 
elseif slot == 1.0 then frag = texture2D(fTexture2, fTexCoord) -- 1, focused
 
end
 
return lF4(fColor*fColor.a)*frag -- alpha
 
end
 
 
local shaderpixelslot=Shader.lua(vshaderpixelslot, fshaderpixelslot, 0,
 
{
 
{name="vMatrix", type=Shader.CMATRIX, sys=Shader.SYS_WVP, vertex=true},
 
{name="fColor", type=Shader.CFLOAT4, sys=Shader.SYS_COLOR, vertex=false}, -- 1st color slot
 
{name="fColor2", type=Shader.CFLOAT4, sys=Shader.SYS_COLOR, vertex=false}, -- 2nd color slot
 
{name="fColor3", type=Shader.CFLOAT4, sys=Shader.SYS_COLOR, vertex=false}, -- 3rd color slot
 
{name="fTexture", type=Shader.CTEXTURE, vertex=false},
 
{name="fTexture2", type=Shader.CTEXTURE, vertex=false},
 
{name="fTexture3", type=Shader.CTEXTURE, vertex=false},
 
{name="slot", type=Shader.CFLOAT, vertex=false},
 
},
 
{
 
{name="vVertex", type=Shader.DFLOAT, mult=2, slot=0, offset=0},
 
{name="vColor", type=Shader.DUBYTE, mult=4, slot=1, offset=0},
 
{name="vTexCoord", type=Shader.DFLOAT, mult=2, slot=2, offset=0},
 
},
 
{
 
{name="fTexCoord", type=Shader.CFLOAT2},
 
}
 
)
 
  
 
-- Class
 
-- Class
Line 86: Line 53:
 
self.params.pixelscalexdown = xparams.pixelscalexdown or self.params.pixelscalexup -- number
 
self.params.pixelscalexdown = xparams.pixelscalexdown or self.params.pixelscalexup -- number
 
self.params.pixelscaleydown = xparams.pixelscaleydown or self.params.pixelscaleyup -- number
 
self.params.pixelscaleydown = xparams.pixelscaleydown or self.params.pixelscaleyup -- number
self.params.pixelwidth = xparams.pixelwidth or 8*3 -- number (autoscale = x padding else width)
+
self.params.pixelwidth = xparams.pixelwidth or 24 -- 24, number (autoscale = x padding else width)
 
self.params.pixelheight = xparams.pixelheight or self.params.pixelwidth -- number (autoscale = y padding else height)
 
self.params.pixelheight = xparams.pixelheight or self.params.pixelwidth -- number (autoscale = y padding else height)
self.params.ninepatch = xparams.ninepatch or 8 -- 0, number
+
self.params.ninepatch = xparams.ninepatch or 16 -- 0, 8, number
 
-- text?
 
-- text?
 
self.params.text = xparams.text or nil -- string
 
self.params.text = xparams.text or nil -- string
Line 110: Line 77:
 
-- audio?
 
-- audio?
 
self.params.sound = xparams.sound or nil -- sound fx
 
self.params.sound = xparams.sound or nil -- sound fx
self.params.volume = xparams.volume or nil -- sound volume
+
self.params.volume = xparams.volume or 0.5 -- sound volume
-- warnings, errors?
+
-- EXTRAS
if self.params.pixelalphaup <= 0 then self.params.pixelalphaup = 0.01 end -- alpha <= 0 breaks shader!
+
self.params.fun = xparams.fun or nil -- function (please check function name if not working!)
if self.params.pixelalphadown <= 0 then self.params.pixelalphadown = 0.01 end -- alpha <= 0 breaks shader!
+
-- set warnings, errors
 +
if self.params.sound and (type(self.params.sound) ~= "table" or self.params.sound.sound == nil) then
 +
print("*** ERROR ***", "ON BUTTON: "..self.selector or "?",
 +
"YOUR SOUND MUST BE A TABLE, SOMETHING LIKE: { sound=Sound.new(\"path/to/your/sound.wav\"), time=0, delay=0.2 }")
 +
self.params.sound = nil
 +
end
 +
if self.params.fun and type(self.params.fun) ~= "function" then
 +
print("*** ERROR ***", "ON BUTTON: "..self.selector or "?", "YOU ARE NOT PASSING A FUNCTION")
 +
if self.params.text then self.params.text = self.params.text.." (error)"
 +
else self.params.text = "error"
 +
end
 +
self.params.ttf = nil self.params.textscalexup = 3 self.params.textscaleyup = 3
 +
self.params.textcolorup = 0xff0000
 +
end
 
-- let's go!
 
-- let's go!
 
self:setButton()
 
self:setButton()
Line 126: Line 106:
 
self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
 
self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
 
self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
 
self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
-- touches event listeners
+
-- touch event listeners
 
self:addEventListener(Event.TOUCHES_BEGIN, self.onTouchesBegin, self)
 
self:addEventListener(Event.TOUCHES_BEGIN, self.onTouchesBegin, self)
 
self:addEventListener(Event.TOUCHES_MOVE, self.onTouchesMove, self)
 
self:addEventListener(Event.TOUCHES_MOVE, self.onTouchesMove, self)
Line 138: Line 118:
 
local textwidth, textheight
 
local textwidth, textheight
 
if self.params.text then
 
if self.params.text then
self.text = TextField.new(self.params.ttf, self.params.text, self.params.text)
+
if self.tf then self:removeChild(self.tf) self.tf = nil end
self.text:setAnchorPoint(0.5, 0.5)
+
self.tf = TextField.new(self.params.ttf, self.params.text, self.params.text)
self.text:setScale(self.params.textscalexup, self.params.textscaleyup)
+
self.tf:setAnchorPoint(0.5, 0.5)
self.text:setTextColor(self.params.textcolorup)
+
self.tf:setScale(self.params.textscalexup, self.params.textscaleyup)
self.text:setAlpha(self.params.textalphaup)
+
self.tf:setTextColor(self.params.textcolorup)
textwidth, textheight = self.text:getWidth(), self.text:getHeight()
+
self.tf:setAlpha(self.params.textalphaup)
 +
textwidth, textheight = self.tf:getWidth(), self.tf:getHeight()
 
end
 
end
 
-- first add pixel
 
-- first add pixel
local pixelimg = false
+
if self.pixel then self:removeChild(self.pixel) self.pixel = nil end
 
if self.params.autoscale and self.params.text then
 
if self.params.autoscale and self.params.text then
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
Line 153: Line 134:
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
 
self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
 
self.params.pixelwidth, self.params.pixelheight)
 
self.params.pixelwidth, self.params.pixelheight)
end
 
if self.params.pixelimgup then
 
self.pixel:setTexture(self.params.pixelimgup, 0) -- Texture, slot
 
pixelimg = true
 
end
 
if self.params.pixelimgdown then
 
self.pixel:setTexture(self.params.pixelimgdown, 1) -- Texture, slot
 
pixelimg = true
 
end
 
if self.params.pixelimgdisabled then
 
self.pixel:setTexture(self.params.pixelimgdisabled, 2) -- Texture, slot
 
pixelimg = true
 
 
end
 
end
 
self.pixel:setScale(self.params.pixelscalexup, self.params.pixelscaleyup)
 
self.pixel:setScale(self.params.pixelscalexup, self.params.pixelscaleyup)
 
self.pixel:setAnchorPoint(0.5, 0.5)
 
self.pixel:setAnchorPoint(0.5, 0.5)
if pixelimg then self.pixel:setShader(shaderpixelslot) end -- apply shader
+
self.pixel:setNinePatch(self.params.ninepatch)
 +
if self.params.pixelimgup then self.pixel:setTexture(self.params.pixelimgup)
 +
elseif self.params.pixelimgdown then self.pixel:setTexture(self.params.pixelimgdown)
 +
elseif self.params.pixelimgdisabled then self.pixel:setTexture(self.params.pixelimgdisabled)
 +
end
 
self:addChild(self.pixel)
 
self:addChild(self.pixel)
 
-- then add text?
 
-- then add text?
if self.params.text then self:addChild(self.text) end
+
if self.params.text then self:addChild(self.tf) end
 
-- finally add tooltip?
 
-- finally add tooltip?
 
if self.params.tooltiptext then
 
if self.params.tooltiptext then
self.ttiptext = TextField.new(self.params.tooltipttf, self.params.tooltiptext, self.params.tooltiptext)
+
if self.ttiptf then
self.ttiptext:setScale(self.params.tooltiptextscale)
+
if self.tooltiplayer then self.tooltiplayer:removeChild(self.ttiptf)
self.ttiptext:setTextColor(self.params.tooltiptextcolor)
+
else self:removeChild(self.ttiptf)
self.ttiptext:setVisible(false)
+
end self.ttiptf = nil
if self.tooltiplayer then self.tooltiplayer:addChild(self.ttiptext)
+
end
else self:addChild(self.ttiptext)
+
self.ttiptf = TextField.new(self.params.tooltipttf, self.params.tooltiptext, self.params.tooltiptext)
 +
self.ttiptf:setScale(self.params.tooltiptextscale)
 +
self.ttiptf:setTextColor(self.params.tooltiptextcolor)
 +
self.ttiptf:setVisible(false)
 +
if self.tooltiplayer then self.tooltiplayer:addChild(self.ttiptf)
 +
else self:addChild(self.ttiptf)
 
end
 
end
 
end
 
end
Line 186: Line 164:
 
function ButtonMonster:updateVisualState()
 
function ButtonMonster:updateVisualState()
 
local function visualState(btn, btnscalex, btnscaley, btnalpha, textcolor, textscalex, textscaley,
 
local function visualState(btn, btnscalex, btnscaley, btnalpha, textcolor, textscalex, textscaley,
pixtexslot, pixelcolor, pixelalpha, pixelscalex, pixelscaley)
+
pixeltex, pixelcolor, pixelalpha, pixelscalex, pixelscaley)
 
btn:setScale(btnscalex, btnscaley)
 
btn:setScale(btnscalex, btnscaley)
 
btn:setAlpha(btnalpha)
 
btn:setAlpha(btnalpha)
 
if btn.params.text then
 
if btn.params.text then
btn.text:setTextColor(textcolor)
+
btn.tf:setTextColor(textcolor)
btn.text:setScale(textscalex, textscaley)
+
btn.tf:setScale(textscalex, textscaley)
end
 
if btn.params.pixelimgup then -- texture
 
local r, g, b = (pixelcolor >> 16 & 0xff) / 255, (pixelcolor >> 8 & 0xff) / 255, (pixelcolor & 0xff) / 255
 
btn.pixel:setShaderConstant("slot", Shader.CFLOAT, 1, pixtexslot) -- set Pixel texture slot
 
btn.pixel:setShaderConstant("fColor", Shader.CFLOAT4, 1, r, g, b, pixelalpha) -- set Pixel color
 
else
 
btn.pixel:setColor(pixelcolor, pixelalpha)
 
 
end
 
end
 +
if pixeltex then btn.pixel:setTexture(pixeltex) end
 +
btn.pixel:setColor(pixelcolor, pixelalpha)
 
btn.pixel:setScale(pixelscalex, pixelscaley)
 
btn.pixel:setScale(pixelscalex, pixelscaley)
 
end
 
end
Line 208: Line 181:
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
v.params.textcolordisabled, v.params.textscalexdown, v.params.textscaleydown,
 
v.params.textcolordisabled, v.params.textscalexdown, v.params.textscaleydown,
2.0, v.params.pixelcolordisabled, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
+
v.params.pixelimgdisabled, v.params.pixelcolordisabled,
-- if v.ttiptext and not v.disabled then -- OPTION 1: hides tooltip when button is Disabled
+
v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
if v.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
+
-- if v.ttiptf and not v.disabled then -- OPTION 1: hides tooltip when button is Disabled
v.ttiptext:setText("("..v.params.tooltiptext..")") -- extra!
+
if v.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
if k == v.currselector then v.ttiptext:setVisible(true)
+
v.ttiptf:setText("("..v.params.tooltiptext..")") -- extra!
else v.ttiptext:setVisible(false)
+
if k == v.currselector then v.ttiptf:setVisible(true)
 +
else v.ttiptf:setVisible(false)
 
end
 
end
 
end
 
end
Line 219: Line 193:
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
 
v.params.textcolordown, v.params.textscalexdown, v.params.textscaleydown,
 
v.params.textcolordown, v.params.textscalexdown, v.params.textscaleydown,
1.0, v.params.pixelcolordown, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
+
v.params.pixelimgdown, v.params.pixelcolordown,
if v.ttiptext then
+
v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
v.ttiptext:setText(v.params.tooltiptext)
+
if v.ttiptf then
 +
v.ttiptf:setText(v.params.tooltiptext)
 
if v.tooltiplayer then -- reset tooltip text position
 
if v.tooltiplayer then -- reset tooltip text position
v.ttiptext:setPosition(
+
v.ttiptf:setPosition(
 
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety)
 
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety)
 
else
 
else
v.ttiptext:setPosition(v:globalToLocal(
+
v.ttiptf:setPosition(v:globalToLocal(
 
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety))
 
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety))
 
end
 
end
v.ttiptext:setVisible(true)
+
v.ttiptf:setVisible(true)
 
end
 
end
 
else -- upState
 
else -- upState
 
visualState(v, v.params.btnscalexup, v.params.btnscaleyup, v.params.btnalphaup,
 
visualState(v, v.params.btnscalexup, v.params.btnscaleyup, v.params.btnalphaup,
 
v.params.textcolorup, v.params.textscalexup, v.params.textscaleyup,
 
v.params.textcolorup, v.params.textscalexup, v.params.textscaleyup,
0.0, v.params.pixelcolorup, v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
+
v.params.pixelimgup, v.params.pixelcolorup,
if v.ttiptext then v.ttiptext:setVisible(false) end
+
v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
 +
if v.ttiptf then v.ttiptf:setVisible(false) end
 
end
 
end
 
end
 
end
Line 243: Line 219:
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
self.params.textcolordisabled, self.params.textscalexdown, self.params.textscaleydown,
 
self.params.textcolordisabled, self.params.textscalexdown, self.params.textscaleydown,
2.0, self.params.pixelcolordisabled, self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
+
self.params.pixelimgdisabled, self.params.pixelcolordisabled,
-- if self.ttiptext and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
+
self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
if self.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
+
-- if self.ttiptf and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
self.ttiptext:setText("("..self.params.tooltiptext..")") -- extra!
+
if self.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
if self.focus then self.ttiptext:setVisible(true)
+
self.ttiptf:setText("("..self.params.tooltiptext..")") -- extra!
else self.ttiptext:setVisible(false)
+
if self.focus then self.ttiptf:setVisible(true)
 +
else self.ttiptf:setVisible(false)
 
end
 
end
 
end
 
end
Line 254: Line 231:
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
 
self.params.textcolordown, self.params.textscalexdown, self.params.textscaleydown,
 
self.params.textcolordown, self.params.textscalexdown, self.params.textscaleydown,
1.0, self.params.pixelcolordown, self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
+
self.params.pixelimgdown, self.params.pixelcolordown,
if self.ttiptext then
+
self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
self.ttiptext:setText(self.params.tooltiptext)
+
if self.ttiptf then
self.ttiptext:setVisible(true)
+
self.ttiptf:setText(self.params.tooltiptext)
 +
self.ttiptf:setVisible(true)
 
end
 
end
 
else -- upState
 
else -- upState
 
visualState(self, self.params.btnscalexup, self.params.btnscaleyup, self.params.btnalphaup,
 
visualState(self, self.params.btnscalexup, self.params.btnscaleyup, self.params.btnalphaup,
 
self.params.textcolorup, self.params.textscalexup, self.params.textscaleyup,
 
self.params.textcolorup, self.params.textscalexup, self.params.textscaleyup,
0.0, self.params.pixelcolorup, self.params.pixelalphaup, self.params.pixelscalexup, self.params.pixelscaleyup)
+
self.params.pixelimgup, self.params.pixelcolorup,
if self.ttiptext then self.ttiptext:setVisible(false) end
+
self.params.pixelalphaup, self.params.pixelscalexup, self.params.pixelscaleyup)
 +
if self.ttiptf then self.ttiptf:setVisible(false) end
 
end
 
end
 
end
 
end
 
end
 
end
  
-- DISABLED
+
-- SOME UTILITY FUNCTIONS
 +
function ButtonMonster:changeText(xtext)
 +
self.params.text = xtext
 +
self:setButton()
 +
end
 +
function ButtonMonster:getText()
 +
return self.params.text
 +
end
 
function ButtonMonster:setDisabled(disabled)
 
function ButtonMonster:setDisabled(disabled)
 
if self.disabled == disabled then return end
 
if self.disabled == disabled then return end
Line 281: Line 267:
 
-- MOUSE
 
-- MOUSE
 
function ButtonMonster:onMouseDown(ev) -- both mouse and touch
 
function ButtonMonster:onMouseDown(ev) -- both mouse and touch
if self:hitTestPoint(ev.x, ev.y, true) then
+
if self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
 
self.focus = true
 
self.focus = true
 
if self.btns then -- update keyboard button id selector
 
if self.btns then -- update keyboard button id selector
Line 287: Line 273:
 
end
 
end
 
self:updateVisualState()
 
self:updateVisualState()
self:selectionSfx() -- play sound fx
+
-- self:selectionSfx() -- play sound fx, you choose!
if self.ttiptext then -- set tooltip initial position
+
local e = Event.new("pressed")
 +
e.currselector = self.selector -- update button id selector
 +
e.disabled = self.disabled -- update button disabled
 +
self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
 +
-- if self.params.fun then self.params.fun(self:getParent()) end -- YOU CAN ADD THIS HERE
 +
if self.ttiptf then -- set tooltip initial position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
self.ttiptext:setPosition(
+
self.ttiptf:setPosition(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
else
 
else
self.ttiptext:setPosition(self:globalToLocal(
+
self.ttiptf:setPosition(self:globalToLocal(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
end
 
end
Line 302: Line 293:
 
function ButtonMonster:onMouseMove(ev) -- both mouse and touch
 
function ButtonMonster:onMouseMove(ev) -- both mouse and touch
 
if self.focus then
 
if self.focus then
if self.ttiptext then -- tooltip follows position
+
if self.ttiptf then -- tooltip follows position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
self.ttiptext:setPosition(
+
self.ttiptf:setPosition(
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 
else
 
else
self.ttiptext:setPosition(self:globalToLocal(
+
self.ttiptf:setPosition(self:globalToLocal(
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
 
end
 
end
 
end
 
end
if not self:hitTestPoint(ev.x, ev.y, true) then
+
if not self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
 
self.focus = false
 
self.focus = false
 
self:updateVisualState()
 
self:updateVisualState()
 
end
 
end
 
ev:stopPropagation()
 
ev:stopPropagation()
elseif self.ttiptext then -- reset tooltip text position
+
elseif self.ttiptf then -- reset tooltip text position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
self.ttiptext:setPosition(
+
self.ttiptf:setPosition(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
else
 
else
self.ttiptext:setPosition(self:globalToLocal(
+
self.ttiptf:setPosition(self:globalToLocal(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
end
 
end
Line 334: Line 325:
 
e.disabled = self.disabled -- update button disabled
 
e.disabled = self.disabled -- update button disabled
 
self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
 
self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
 +
if self.params.fun then self.params.fun(self:getParent()) end -- OR EVEN HERE?
 
ev:stopPropagation()
 
ev:stopPropagation()
 
end
 
end
Line 341: Line 333:
 
self.focus = true
 
self.focus = true
 
self.hover = true
 
self.hover = true
if self.ttiptext then -- tooltip follows mouse position
+
if self.ttiptf then -- tooltip follows mouse position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
self.ttiptext:setPosition(
+
self.ttiptf:setPosition(
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 
else
 
else
self.ttiptext:setPosition(self:globalToLocal(
+
self.ttiptf:setPosition(self:globalToLocal(
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
 
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
 
end
 
end
Line 361: Line 353:
 
e.disabled = self.disabled -- update button disabled
 
e.disabled = self.disabled -- update button disabled
 
self:dispatchEvent(e)
 
self:dispatchEvent(e)
-- self:selectionSfx() -- play sound fx? you choose!
+
self:selectionSfx() -- play sound fx? you choose!
 
-- trick to remove residuals when fast moving mouse
 
-- trick to remove residuals when fast moving mouse
local timer = Timer.new(100*1, 1) -- number of repetition, the higher the safer
+
local timer = Timer.new(100*1, 1) -- number of repetition, the higher the safer, (delay,repeatCount)
 
timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
 
timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
 
timer:start()
 
timer:start()
 +
-- if self.params.fun then self.params.fun(self:getParent()) end -- HERE COULD ALSO BE USEFUL?
 
else
 
else
 
self.onexit = true
 
self.onexit = true
Line 375: Line 368:
 
self.onenter = false
 
self.onenter = false
 
self.moving = false
 
self.moving = false
if self.onexit then
+
if self.onexit then -- execute onexit code only once
-- execute onexit code only once
 
 
self.onexit = false
 
self.onexit = false
 
self:updateVisualState()
 
self:updateVisualState()
Line 386: Line 378:
 
-- if button is on focus, stop propagation of touch events
 
-- if button is on focus, stop propagation of touch events
 
function ButtonMonster:onTouchesBegin(ev) -- touch only
 
function ButtonMonster:onTouchesBegin(ev) -- touch only
if self.focus then ev:stopPropagation() end
+
if self.focus then
 +
ev:stopPropagation()
 +
end
 
end
 
end
 
-- if button is on focus, stop propagation of touch events
 
-- if button is on focus, stop propagation of touch events
 
function ButtonMonster:onTouchesMove(ev) -- touch only
 
function ButtonMonster:onTouchesMove(ev) -- touch only
if self.focus then ev:stopPropagation() end
+
if self.focus then
 +
ev:stopPropagation()
 +
end
 
end
 
end
 
-- if button is on focus, stop propagation of touch events
 
-- if button is on focus, stop propagation of touch events
Line 396: Line 392:
 
if self.focus then
 
if self.focus then
 
ev:stopPropagation()
 
ev:stopPropagation()
elseif self.ttiptext then -- reset tooltip text position
+
elseif self.ttiptf then -- reset tooltip text position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
self.ttiptext:setPosition(
+
self.ttiptf:setPosition(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
else
 
else
self.ttiptext:setPosition(self:globalToLocal(
+
self.ttiptf:setPosition(self:globalToLocal(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
end
 
end
Line 414: Line 410:
 
end
 
end
 
self:updateVisualState()
 
self:updateVisualState()
elseif self.ttiptext then -- reset tooltip text position
+
elseif self.ttiptf then -- reset tooltip text position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
self.ttiptext:setPosition(
+
self.ttiptf:setPosition(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 
else
 
else
self.ttiptext:setPosition(self:globalToLocal(
+
self.ttiptf:setPosition(self:globalToLocal(
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 
end
 
end
Line 432: Line 428:
 
local prev = snd.time
 
local prev = snd.time
 
if curr - prev > snd.delay then
 
if curr - prev > snd.delay then
snd.sound:play():setVolume(self.params.volume)
+
local channel = snd.sound:play()
 +
if channel then
 +
channel:setVolume(self.params.volume)
 +
end
 
snd.time = curr
 
snd.time = curr
 
end
 
end
 
end
 
end
 +
end
 +
function ButtonMonster:setVolume(xvolume)
 +
self.params.volume = xvolume
 +
end
 +
function ButtonMonster:getVolume()
 +
return self.params.volume
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
Line 445: Line 450:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 
-- ButtonMonster DEMO 1
 
-- ButtonMonster DEMO 1
 +
-- bg
 
application:setBackgroundColor(0x6c6c6c)
 
application:setBackgroundColor(0x6c6c6c)
 
 
-- font
 
-- font
 
local myttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 20)
 
local myttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 20)
local myttipttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 18)
 
 
-- textures
 
-- textures
 
local btnuptex = Texture.new("gfx/ui/btn_01_up.png")
 
local btnuptex = Texture.new("gfx/ui/btn_01_up.png")
Line 460: Line 464:
 
local btn01 = ButtonMonster.new({
 
local btn01 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
pixelscaleyup=0.7, pixelscaleydown=1,
 
text="button 1", ttf=myttf,
 
text="button 1", ttf=myttf,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
Line 465: Line 470:
 
local btn02 = ButtonMonster.new({
 
local btn02 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
pixelscaleyup=0.7, pixelscaleydown=1,
 
text="button 2", ttf=myttf,
 
text="button 2", ttf=myttf,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
Line 470: Line 476:
 
local btn03 = ButtonMonster.new({
 
local btn03 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
pixelscaleyup=0.7, pixelscaleydown=1,
 
text="button 3", ttf=myttf,
 
text="button 3", ttf=myttf,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
Line 475: Line 482:
 
local btn04 = ButtonMonster.new({
 
local btn04 = ButtonMonster.new({
 
autoscale=false,
 
autoscale=false,
pixelwidth=8*5, pixelheight=8*24,
+
pixelwidth=8*6, pixelheight=8*30,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
pixelscalexup=0.7, pixelscalexdown=1,
 
pixelcolordown=0x00ff00,
 
pixelcolordown=0x00ff00,
 
text="b\nt\nn\n \n4", ttf=myttf,
 
text="b\nt\nn\n \n4", ttf=myttf,
Line 516: Line 524:
  
 
'''Demo 2'''
 
'''Demo 2'''
 +
 +
Keyboard navigation (arrow keys + ENTER).
  
 
{{#widget:GApp|app=ButtonMonsterDemo2x.GApp|width=480|height=320}}
 
{{#widget:GApp|app=ButtonMonsterDemo2x.GApp|width=480|height=320}}
Line 563: Line 573:
 
pixelwidth=8*5, pixelheight=8*24,
 
pixelwidth=8*5, pixelheight=8*24,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
ninepatch=32,
 
pixelcolordown=0x00ff00,
 
pixelcolordown=0x00ff00,
 
text="b\nt\nn\n \n4", ttf=myttf,
 
text="b\nt\nn\n \n4", ttf=myttf,

Latest revision as of 02:31, 9 November 2024

This ButtonMonster class allows you to build any button, from the simplest (text) to the most complicated (tooltip, keyboard navigation). It is optimised for menus and in games.

Media:buttonMonster.lua (tip: right click and save link as)

ButtonMonster Class

--[[
-- ButtonMonster
-- mokalux, cc0
-- Pixel, Image, 9patch, Text, Tooltip,
-- Up, Down, Disabled, Hover,
-- Sfx, Touch, Mouse and Keyboard navigation!
-- Functions
v 0.2.2: 2024-11-08 selectionSfx(): added channel checks before setting volume
v 0.2.1: 2024-04-22 added function callback (functions with no parameters for now!)
v 0.2.0: 2023-12-01 terminator, should be fine in games too now
v 0.1.0: 2021-06-01 total recall, this class has become a Monster! best used in menus but who knows?
v 0.0.1: 2020-03-28 init (based on the initial generic Gideros Button class)
]]

-- Class
ButtonMonster = Core.class(Sprite)

function ButtonMonster:init(xparams, xselector, xttlayer)
	-- user params
	self.params = xparams or {}
	-- add keyboard navigation?
	self.selector = xselector or nil -- button id selector
	self.btns = nil -- assign this value directly from your code (you assign it a list of buttons)
	-- add a layer for the tooltip?
	self.tooltiplayer = xttlayer or nil
	-- button params
	self.params.autoscale = xparams.autoscale or (xparams.autoscale == nil) -- bool (default = true)
	self.params.btnscalexup = xparams.btnscalexup or 1 -- number
	self.params.btnscaleyup = xparams.btnscaleyup or self.params.btnscalexup -- number
	self.params.btnscalexdown = xparams.btnscalexdown or self.params.btnscalexup -- number
	self.params.btnscaleydown = xparams.btnscaleydown or self.params.btnscaleyup -- number
	self.params.btnalphaup = xparams.btnalphaup or 1 -- number
	self.params.btnalphadown = xparams.btnalphadown or self.params.btnalphaup -- number
	-- pixel?
	self.params.pixelcolorup = xparams.pixelcolorup or 0xffffff -- color
	self.params.pixelcolordown = xparams.pixelcolordown or self.params.pixelcolorup -- color
	self.params.pixelcolordisabled = xparams.pixelcolordisabled or 0x555555 -- color
	self.params.pixelimgup = xparams.pixelimgup or nil -- img Up Texture
	self.params.pixelimgdown = xparams.pixelimgdown or self.params.pixelimgup -- img Down Texture
	self.params.pixelimgdisabled = xparams.pixelimgdisabled or self.params.pixelimgup -- img Disabled Texture
	self.params.pixelalphaup = xparams.pixelalphaup or 1 -- number
	self.params.pixelalphadown = xparams.pixelalphadown or self.params.pixelalphaup -- number
	self.params.pixelscalexup = xparams.pixelscalexup or 1 -- number
	self.params.pixelscaleyup = xparams.pixelscaleyup or self.params.pixelscalexup -- number
	self.params.pixelscalexdown = xparams.pixelscalexdown or self.params.pixelscalexup -- number
	self.params.pixelscaleydown = xparams.pixelscaleydown or self.params.pixelscaleyup -- number
	self.params.pixelwidth = xparams.pixelwidth or 24 -- 24, number (autoscale = x padding else width)
	self.params.pixelheight = xparams.pixelheight or self.params.pixelwidth -- number (autoscale = y padding else height)
	self.params.ninepatch = xparams.ninepatch or 16 -- 0, 8, number
	-- text?
	self.params.text = xparams.text or nil -- string
	self.params.ttf = xparams.ttf or nil -- ttf font
	self.params.textcolorup = xparams.textcolorup or 0x0 -- color
	self.params.textcolordown = xparams.textcolordown or self.params.textcolorup -- color
	self.params.textcolordisabled = xparams.textcolordisabled or 0x777777 -- color
	self.params.textalphaup = xparams.textalphaup or 1 -- number
	self.params.textalphadown = xparams.textalphaup or self.params.textalphaup -- number
	self.params.textscalexup = xparams.textscalexup or 1 -- number
	self.params.textscaleyup = xparams.textscaleyup or self.params.textscalexup -- number
	self.params.textscalexdown = xparams.textscalexdown or self.params.textscalexup -- number
	self.params.textscaleydown = xparams.textscaleydown or self.params.textscaleyup -- number
	-- tool tip?
	self.params.tooltiptext = xparams.tooltiptext or nil -- string
	self.params.tooltipttf = xparams.tooltipttf or nil -- ttf font
	self.params.tooltiptextcolor = xparams.tooltiptextcolor or 0x0 -- color
	self.params.tooltiptextscale = xparams.tooltiptextscale or 1 -- number
	self.params.tooltipoffsetx = xparams.tooltipoffsetx or 0 -- number
	self.params.tooltipoffsety = xparams.tooltipoffsety or 0 -- self.params.tooltipoffsetx -- number
	-- audio?
	self.params.sound = xparams.sound or nil -- sound fx
	self.params.volume = xparams.volume or 0.5 -- sound volume
	-- EXTRAS
	self.params.fun = xparams.fun or nil -- function (please check function name if not working!)
	-- set warnings, errors
	if self.params.sound and (type(self.params.sound) ~= "table" or self.params.sound.sound == nil) then
		print("*** ERROR ***", "ON BUTTON: "..self.selector or "?",
			"YOUR SOUND MUST BE A TABLE, SOMETHING LIKE: { sound=Sound.new(\"path/to/your/sound.wav\"), time=0, delay=0.2 }")
		self.params.sound = nil
	end
	if self.params.fun and type(self.params.fun) ~= "function" then
		print("*** ERROR ***", "ON BUTTON: "..self.selector or "?", "YOU ARE NOT PASSING A FUNCTION")
		if self.params.text then self.params.text = self.params.text.." (error)"
		else self.params.text = "error"
		end
		self.params.ttf = nil self.params.textscalexup = 3 self.params.textscaleyup = 3
		self.params.textcolorup = 0xff0000
	end
	-- let's go!
	self:setButton()
	-- update visual state
	self.focus = false
	self.hover = false
	self.disabled = false
	self:updateVisualState()
	-- mouse event listeners
	self:addEventListener(Event.MOUSE_DOWN, self.onMouseDown, self)
	self:addEventListener(Event.MOUSE_MOVE, self.onMouseMove, self)
	self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
	self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
	-- touch event listeners
	self:addEventListener(Event.TOUCHES_BEGIN, self.onTouchesBegin, self)
	self:addEventListener(Event.TOUCHES_MOVE, self.onTouchesMove, self)
	self:addEventListener(Event.TOUCHES_END, self.onTouchesEnd, self)
	self:addEventListener(Event.TOUCHES_CANCEL, self.onTouchesCancel, self)
end

-- FUNCTIONS
function ButtonMonster:setButton()
	-- text dimensions
	local textwidth, textheight
	if self.params.text then
		if self.tf then self:removeChild(self.tf) self.tf = nil end
		self.tf = TextField.new(self.params.ttf, self.params.text, self.params.text)
		self.tf:setAnchorPoint(0.5, 0.5)
		self.tf:setScale(self.params.textscalexup, self.params.textscaleyup)
		self.tf:setTextColor(self.params.textcolorup)
		self.tf:setAlpha(self.params.textalphaup)
		textwidth, textheight = self.tf:getWidth(), self.tf:getHeight()
	end
	-- first add pixel
	if self.pixel then self:removeChild(self.pixel) self.pixel = nil end
	if self.params.autoscale and self.params.text then
		self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
			textwidth+self.params.pixelwidth, textheight+self.params.pixelheight)
	else
		self.pixel = Pixel.new(self.params.pixelcolorup, self.params.pixelalphaup,
			self.params.pixelwidth, self.params.pixelheight)
	end
	self.pixel:setScale(self.params.pixelscalexup, self.params.pixelscaleyup)
	self.pixel:setAnchorPoint(0.5, 0.5)
	self.pixel:setNinePatch(self.params.ninepatch)
	if self.params.pixelimgup then self.pixel:setTexture(self.params.pixelimgup)
	elseif self.params.pixelimgdown then self.pixel:setTexture(self.params.pixelimgdown)
	elseif self.params.pixelimgdisabled then self.pixel:setTexture(self.params.pixelimgdisabled)
	end
	self:addChild(self.pixel)
	-- then add text?
	if self.params.text then self:addChild(self.tf) end
	-- finally add tooltip?
	if self.params.tooltiptext then
		if self.ttiptf then
			if self.tooltiplayer then self.tooltiplayer:removeChild(self.ttiptf)
			else self:removeChild(self.ttiptf)
			end self.ttiptf = nil
		end
		self.ttiptf = TextField.new(self.params.tooltipttf, self.params.tooltiptext, self.params.tooltiptext)
		self.ttiptf:setScale(self.params.tooltiptextscale)
		self.ttiptf:setTextColor(self.params.tooltiptextcolor)
		self.ttiptf:setVisible(false)
		if self.tooltiplayer then self.tooltiplayer:addChild(self.ttiptf)
		else self:addChild(self.ttiptf)
		end
	end
end

function ButtonMonster:updateVisualState()
	local function visualState(btn, btnscalex, btnscaley, btnalpha, textcolor, textscalex, textscaley,
			pixeltex, pixelcolor, pixelalpha, pixelscalex, pixelscaley)
		btn:setScale(btnscalex, btnscaley)
		btn:setAlpha(btnalpha)
		if btn.params.text then
			btn.tf:setTextColor(textcolor)
			btn.tf:setScale(textscalex, textscaley)
		end
		if pixeltex then btn.pixel:setTexture(pixeltex) end
		btn.pixel:setColor(pixelcolor, pixelalpha)
		btn.pixel:setScale(pixelscalex, pixelscaley)
	end
	if self.btns then
--		print("KEYBOARD NAVIGATION")
		for k, v in ipairs(self.btns) do
			if v.disabled then -- disabledState
				visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
					v.params.textcolordisabled, v.params.textscalexdown, v.params.textscaleydown,
					v.params.pixelimgdisabled, v.params.pixelcolordisabled,
					v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
--				if v.ttiptf and not v.disabled then -- OPTION 1: hides tooltip when button is Disabled
				if v.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
					v.ttiptf:setText("("..v.params.tooltiptext..")") -- extra!
					if k == v.currselector then v.ttiptf:setVisible(true)
					else v.ttiptf:setVisible(false)
					end
				end
			elseif k == v.currselector then -- downState
				visualState(v, v.params.btnscalexdown, v.params.btnscaleydown, v.params.btnalphadown,
					v.params.textcolordown, v.params.textscalexdown, v.params.textscaleydown,
					v.params.pixelimgdown, v.params.pixelcolordown,
					v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
				if v.ttiptf then
					v.ttiptf:setText(v.params.tooltiptext)
					if v.tooltiplayer then -- reset tooltip text position
						v.ttiptf:setPosition(
							v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety)
					else
						v.ttiptf:setPosition(v:globalToLocal(
							v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety))
					end
					v.ttiptf:setVisible(true)
				end
			else -- upState
				visualState(v, v.params.btnscalexup, v.params.btnscaleyup, v.params.btnalphaup,
					v.params.textcolorup, v.params.textscalexup, v.params.textscaleyup,
					v.params.pixelimgup, v.params.pixelcolorup,
					v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
				if v.ttiptf then v.ttiptf:setVisible(false) end
			end
		end
	else
--		print("TOUCH, MOUSE NAVIGATION")
		if self.disabled then -- disabledState
			visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
				self.params.textcolordisabled, self.params.textscalexdown, self.params.textscaleydown,
				self.params.pixelimgdisabled, self.params.pixelcolordisabled,
				self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
--			if self.ttiptf and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
			if self.ttiptf then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
				self.ttiptf:setText("("..self.params.tooltiptext..")") -- extra!
				if self.focus then self.ttiptf:setVisible(true)
				else self.ttiptf:setVisible(false)
				end
			end
		elseif self.focus or self.hover then -- downState
			visualState(self, self.params.btnscalexdown, self.params.btnscaleydown, self.params.btnalphadown,
				self.params.textcolordown, self.params.textscalexdown, self.params.textscaleydown,
				self.params.pixelimgdown, self.params.pixelcolordown,
				self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
			if self.ttiptf then
				self.ttiptf:setText(self.params.tooltiptext)
				self.ttiptf:setVisible(true)
			end
		else -- upState
			visualState(self, self.params.btnscalexup, self.params.btnscaleyup, self.params.btnalphaup,
				self.params.textcolorup, self.params.textscalexup, self.params.textscaleyup,
				self.params.pixelimgup, self.params.pixelcolorup,
				self.params.pixelalphaup, self.params.pixelscalexup, self.params.pixelscaleyup)
			if self.ttiptf then self.ttiptf:setVisible(false) end
		end
	end
end

-- SOME UTILITY FUNCTIONS
function ButtonMonster:changeText(xtext)
	self.params.text = xtext
	self:setButton()
end
function ButtonMonster:getText()
	return self.params.text
end
function ButtonMonster:setDisabled(disabled)
	if self.disabled == disabled then return end
	self.disabled = disabled
	self:updateVisualState()
end
function ButtonMonster:getDisabled()
	return self.disabled
end

-- LISTENERS
-- MOUSE
function ButtonMonster:onMouseDown(ev) -- both mouse and touch
	if self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
		self.focus = true
		if self.btns then -- update keyboard button id selector
			for k, v in ipairs(self.btns) do v.currselector = self.selector end
		end
		self:updateVisualState()
--		self:selectionSfx() -- play sound fx, you choose!
		local e = Event.new("pressed")
		e.currselector = self.selector -- update button id selector
		e.disabled = self.disabled -- update button disabled
		self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
--		if self.params.fun then self.params.fun(self:getParent()) end -- YOU CAN ADD THIS HERE
		if self.ttiptf then -- set tooltip initial position
			if self.tooltiplayer then
				self.ttiptf:setPosition(
					self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
			else
				self.ttiptf:setPosition(self:globalToLocal(
					self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
			end
		end
		ev:stopPropagation()
	end
end
function ButtonMonster:onMouseMove(ev) -- both mouse and touch
	if self.focus then
		if self.ttiptf then -- tooltip follows position
			if self.tooltiplayer then
				self.ttiptf:setPosition(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
			else
				self.ttiptf:setPosition(self:globalToLocal(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
			end
		end
		if not self:hitTestPoint(ev.x, ev.y, true) then -- (x,y,onlyvisible,ref)
			self.focus = false
			self:updateVisualState()
		end
		ev:stopPropagation()
	elseif self.ttiptf then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptf:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptf:setPosition(self:globalToLocal(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
		end
	end
end
function ButtonMonster:onMouseUp(ev) -- both mouse and touch
	if self.focus then
		self.focus = false
		self:updateVisualState()
		local e = Event.new("clicked")
		e.currselector = self.selector -- update button id selector
		e.disabled = self.disabled -- update button disabled
		self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
		if self.params.fun then self.params.fun(self:getParent()) end -- OR EVEN HERE?
		ev:stopPropagation()
	end
end
function ButtonMonster:onMouseHover(ev) -- mouse only
	if self:hitTestPoint(ev.x, ev.y, true) then -- onenter
		self.focus = true
		self.hover = true
		if self.ttiptf then -- tooltip follows mouse position
			if self.tooltiplayer then
				self.ttiptf:setPosition(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
			else
				self.ttiptf:setPosition(self:globalToLocal(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety))
			end
		end
		-- execute onenter code only once
		self.onenter = not self.onenter
		if not self.onenter then self.moving = true end
		if not self.moving then
			if self.btns then -- update keyboard button id selector
				for k, v in ipairs(self.btns) do v.currselector = self.selector end
			end
			local e = Event.new("hovered") -- dispatch "hovered" event
			e.currselector = self.selector -- update button id selector
			e.disabled = self.disabled -- update button disabled
			self:dispatchEvent(e)
			self:selectionSfx() -- play sound fx? you choose!
			-- trick to remove residuals when fast moving mouse
			local timer = Timer.new(100*1, 1) -- number of repetition, the higher the safer, (delay,repeatCount)
			timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
			timer:start()
--			if self.params.fun then self.params.fun(self:getParent()) end -- HERE COULD ALSO BE USEFUL?
		else
			self.onexit = true
		end
		ev:stopPropagation()
	else -- onexit
		self.focus = false
		self.hover = false
		self.onenter = false
		self.moving = false
		if self.onexit then -- execute onexit code only once
			self.onexit = false
			self:updateVisualState()
		end
	end
end

-- TOUCHES
-- if button is on focus, stop propagation of touch events
function ButtonMonster:onTouchesBegin(ev) -- touch only
	if self.focus then
		ev:stopPropagation()
	end
end
-- if button is on focus, stop propagation of touch events
function ButtonMonster:onTouchesMove(ev) -- touch only
	if self.focus then
		ev:stopPropagation()
	end
end
-- if button is on focus, stop propagation of touch events
function ButtonMonster:onTouchesEnd(ev) -- touch only
	if self.focus then
		ev:stopPropagation()
	elseif self.ttiptf then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptf:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptf:setPosition(self:globalToLocal(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
		end
	end
end
-- if touches are cancelled, reset the state of the button
function ButtonMonster:onTouchesCancel(ev) -- app interrupted (phone call, ...), touch only
	if self.focus then
		self.focus = false
		if self.btns then -- update keyboard button id selector
			for k, v in ipairs(self.btns) do v.currselector = self.selector end
		end
		self:updateVisualState()
	elseif self.ttiptf then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptf:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptf:setPosition(self:globalToLocal(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
		end
	end
end

-- AUDIO
function ButtonMonster:selectionSfx()
	if self.params.sound then
		local snd = self.params.sound
		local curr = os.timer()
		local prev = snd.time
		if curr - prev > snd.delay then
			local channel = snd.sound:play()
			if channel then
				channel:setVolume(self.params.volume)
			end
			snd.time = curr
		end
	end
end
function ButtonMonster:setVolume(xvolume)
	self.params.volume = xvolume
end
function ButtonMonster:getVolume()
	return self.params.volume
end

ButtonMonster Demos

Demo 1

-- ButtonMonster DEMO 1
-- bg
application:setBackgroundColor(0x6c6c6c)
-- font
local myttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 20)
-- textures
local btnuptex = Texture.new("gfx/ui/btn_01_up.png")
local btndowntex = Texture.new("gfx/ui/btn_01_down.png")
local btndisabledtex = Texture.new("gfx/ui/btn_01_disabled.png")
-- buttons sound
local btnsound = {sound=Sound.new("audio/Braam - Retro Pulse.wav"), time=0, delay=0.5} -- delay=0.5, 0.05
local volume = 0.3
-- buttons
local btn01 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscaleyup=0.7, pixelscaleydown=1,
	text="button 1", ttf=myttf,
	sound=btnsound, volume=volume,
}, 1)
local btn02 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscaleyup=0.7, pixelscaleydown=1,
	text="button 2", ttf=myttf,
	sound=btnsound, volume=volume,
}, 2)
local btn03 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscaleyup=0.7, pixelscaleydown=1,
	text="button 3", ttf=myttf,
	sound=btnsound, volume=volume,
}, 3)
local btn04 = ButtonMonster.new({
	autoscale=false,
	pixelwidth=8*6, pixelheight=8*30,
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	pixelscalexup=0.7, pixelscalexdown=1,
	pixelcolordown=0x00ff00,
	text="b\nt\nn\n \n4", ttf=myttf,
	sound=btnsound, volume=volume,
}, 4)
local btnexit = ButtonMonster.new({
	pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
	text="EXIT", ttf=myttf,
})
-- position
btn01:setPosition(16*5, 16*3)
btn02:setPosition(16*5, 16*8)
btn03:setPosition(16*5, 16*12.5)
btn04:setPosition(16*12, 16*8)
btnexit:setPosition(16*24, 16*16)
-- order
stage:addChild(btn01)
stage:addChild(btn02)
stage:addChild(btn03)
stage:addChild(btn04)
stage:addChild(btnexit)

-- add listeners
function clicked(btn)
	print(btn.currselector, btn.disabled)
	if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled())
	elseif btn.currselector == 5 then
		if not application:isPlayerMode() then application:exit()
		else print("EXIT")
		end
	end
end
btn01:addEventListener("clicked", clicked)
btn02:addEventListener("clicked", clicked)
btn03:addEventListener("clicked", clicked)
btn04:addEventListener("clicked", clicked)
btnexit:addEventListener("clicked", clicked)

Demo 2

Keyboard navigation (arrow keys + ENTER).

-- ButtonMonster DEMO 2: keyboard navigation (arrow keys + ENTER)
application:setBackgroundColor(0x6c6c6c)
-- a gradient bg
local gradient = Pixel.new(0xffffff, 1, application:getContentWidth(), application:getContentHeight())
gradient:setColor(0x0, 1, 0xaa5500, 1, 15*16)
gradient:setAnchorPoint(0.5, 0.5)

-- font
local myttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 20)
local myttipttf = TTFont.new("fonts/Cabin-Bold-TTF.ttf", 18)
-- textures
local btnuptex = Texture.new("gfx/ui/btn_01_up.png")
local btndowntex = Texture.new("gfx/ui/btn_01_down.png")
local btndisabledtex = Texture.new("gfx/ui/btn_01_disabled.png")
-- buttons tooltip layer
local tooltiplayer = Sprite.new()
-- buttons sound
local btnsound = {sound=Sound.new("audio/Braam - Retro Pulse.wav"), time=0, delay=0.5} -- delay=0.5, 0.05
local volume = 0.3
-- initial button selected
local selector = 1
-- buttons
local btn01 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 1", ttf=myttf,
	tooltiptext="btn1", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
	sound=btnsound, volume=volume,
}, 1, tooltiplayer)
local btn02 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 2", ttf=myttf,
	tooltiptext="click me!", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
	sound=btnsound, volume=volume,
}, 2, tooltiplayer)
local btn03 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 3", ttf=myttf,
	tooltiptext="btn3", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
	sound=btnsound, volume=volume,
}, 3, tooltiplayer)
local btn04 = ButtonMonster.new({
	autoscale=false,
	pixelwidth=8*5, pixelheight=8*24,
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	ninepatch=32,
	pixelcolordown=0x00ff00,
	text="b\nt\nn\n \n4", ttf=myttf,
	tooltiptext="btn4", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-4, tooltipoffsety=8*12,
	sound=btnsound, volume=volume,
}, 4, tooltiplayer)
local btnexit = ButtonMonster.new({
	pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
	text="EXIT", ttf=myttf,
}, 5, tooltiplayer)
-- keyboard navigation
local btns = {}
btns[#btns + 1] = btn01
btns[#btns + 1] = btn02
btns[#btns + 1] = btn03
btns[#btns + 1] = btn04
btns[#btns + 1] = btnexit
-- position
gradient:setPosition(application:getContentWidth()/2, application:getContentHeight()/2)
btn01:setPosition(16*5, 16*3)
btn02:setPosition(16*5, 16*8)
btn03:setPosition(16*5, 16*12.5)
btn04:setPosition(16*12, 16*8)
btnexit:setPosition(16*24, 16*16)
-- order
stage:addChild(gradient)
for k, v in ipairs(btns) do
	stage:addChild(v)
end
stage:addChild(tooltiplayer)

-- shared listener functions
function clicked(input, btn)
	selector = btn.currselector
	print(input, btn.currselector, btn.disabled)
	if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled())
	elseif btn.currselector == 5 then
		if not application:isPlayerMode() then application:exit()
		else print("EXIT")
		end
	end
end
-- add listeners
for k, v in ipairs(btns) do
	v:addEventListener("clicked", clicked, "mouse", v)
	v:addEventListener("hovered", function(e) selector = e.currselector end)
	v.btns = btns -- list of navigatable buttons
end

-- keyboard handler
function updateButton()
	for k, v in ipairs(btns) do
		v.currselector = selector
		v:updateVisualState()
		if k == selector then v:selectionSfx() end
	end
end
stage:addEventListener(Event.KEY_DOWN, function(e)
	if e.keyCode == KeyCode.UP or e.keyCode == KeyCode.LEFT then
		selector -= 1 if selector < 1 then selector = #btns end updateButton()
	elseif e.keyCode == KeyCode.DOWN or e.keyCode == KeyCode.RIGHT then
		selector += 1 if selector > #btns then selector = 1 end updateButton()
	elseif e.keyCode == KeyCode.ENTER then
		clicked("keyboard", btns[selector])
	end
end)

-- let's go!
updateButton() -- highlight first button


UI Buttons