Difference between revisions of "ButtonMonster class"

From GiderosMobile
(Created page with "__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. '''Note: needs Gideros...")
 
Line 9: Line 9:
 
--[[
 
--[[
 
-- ButtonMonster
 
-- ButtonMonster
-- A Button class with:
+
-- Pixel, Image, 9patch, Text, Tooltip,
-- a Pixel, Image 9patch, Text, Tooltip,
+
-- Up, Down, Disabled, Hover,
-- Up state, Down state, Disabled state,
+
-- Sfx, Touch, Mouse and Keyboard navigation!
-- Hover, Sfx, Mouse and Keyboard navigation!
+
v 0.2.0: 2023-11-29 terminator, should be fine in games too now
v 0.2.0: 2023-11-20 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!
+
-- Shader, please adapt the path accordingly!
 
--!NEEDS:../luashader/luashader.lua
 
--!NEEDS:../luashader/luashader.lua
  
Line 115: Line 114:
 
if self.params.pixelalphaup <= 0 then self.params.pixelalphaup = 0.01 end -- alpha <= 0 breaks shader!
 
if self.params.pixelalphaup <= 0 then self.params.pixelalphaup = 0.01 end -- alpha <= 0 breaks shader!
 
if self.params.pixelalphadown <= 0 then self.params.pixelalphadown = 0.01 end -- alpha <= 0 breaks shader!
 
if self.params.pixelalphadown <= 0 then self.params.pixelalphadown = 0.01 end -- alpha <= 0 breaks shader!
-- button sprite holder
 
self.sprite = Sprite.new()
 
self:addChild(self.sprite)
 
 
-- let's go!
 
-- let's go!
 
self:setButton()
 
self:setButton()
 
-- update visual state
 
-- update visual state
 +
self.focus = false
 +
self.hover = false
 +
self.disabled = false
 
self:updateVisualState()
 
self:updateVisualState()
self.hovered = nil
+
-- mouse event listeners
self.disabled = nil
 
self.ismouse = true
 
-- event listeners
 
 
self:addEventListener(Event.MOUSE_DOWN, self.onMouseDown, self)
 
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_UP, self.onMouseUp, self)
 
self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
 
self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, self)
 +
-- touches 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
 
end
  
Line 153: Line 155:
 
end
 
end
 
if self.params.pixelimgup then
 
if self.params.pixelimgup then
self.pixel:setTexture(self.params.pixelimgup, 0)
+
self.pixel:setTexture(self.params.pixelimgup, 0) -- Texture, slot
 
pixelimg = true
 
pixelimg = true
 
end
 
end
 
if self.params.pixelimgdown then
 
if self.params.pixelimgdown then
self.pixel:setTexture(self.params.pixelimgdown, 1)
+
self.pixel:setTexture(self.params.pixelimgdown, 1) -- Texture, slot
 
pixelimg = true
 
pixelimg = true
 
end
 
end
 
if self.params.pixelimgdisabled then
 
if self.params.pixelimgdisabled then
self.pixel:setTexture(self.params.pixelimgdisabled, 2)
+
self.pixel:setTexture(self.params.pixelimgdisabled, 2) -- Texture, slot
 
pixelimg = true
 
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
+
if pixelimg then self.pixel:setShader(shaderpixelslot) end -- apply shader
self.sprite:addChild(self.pixel)
+
self:addChild(self.pixel)
 
-- then add text?
 
-- then add text?
if self.params.text then self.sprite:addChild(self.text) end
+
if self.params.text then self:addChild(self.text) end
 
-- finally add tooltip?
 
-- finally add tooltip?
 
if self.params.tooltiptext then
 
if self.params.tooltiptext then
Line 182: Line 184:
 
end
 
end
  
-- VISUAL STATE
 
 
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,
Line 200: Line 201:
 
end
 
end
 
btn.pixel:setScale(pixelscalex, pixelscaley)
 
btn.pixel:setScale(pixelscalex, pixelscaley)
end
 
local function vtooltip(index, btn) -- keyboard tooltip visuals
 
-- if btn.ttiptext and not btn.disabled then -- OPTION 1: hides tooltip when button is Disabled
 
if btn.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
 
if index == btn.currselector then -- button is focused
 
if btn.disabled then btn.ttiptext:setText("("..btn.params.tooltiptext..")") -- extra!
 
else btn.ttiptext:setText(btn.params.tooltiptext)
 
end
 
btn.ttiptext:setVisible(true)
 
else -- button is not focused
 
btn.ttiptext:setVisible(false)
 
end
 
if not btn.hovered then -- reposition the tooltip when mouse is not hovering
 
if btn.tooltiplayer then
 
btn.ttiptext:setPosition(
 
btn:getX()+btn.params.tooltipoffsetx, btn:getY()+btn.params.tooltipoffsety)
 
else
 
btn.ttiptext:setPosition(btn:globalToLocal(
 
btn:getX()+btn.params.tooltipoffsetx, btn:getY()+btn.params.tooltipoffsety))
 
end
 
end
 
end
 
 
end
 
end
 
if self.btns then
 
if self.btns then
 
-- print("KEYBOARD NAVIGATION")
 
-- print("KEYBOARD NAVIGATION")
 
for k, v in ipairs(self.btns) do
 
for k, v in ipairs(self.btns) do
if v.disabled then -- button is Disabled
+
if v.disabled then -- disabledState
 
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)
 
2.0, v.params.pixelcolordisabled, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
vtooltip(k, v)
+
-- if v.ttiptext and not v.disabled then -- OPTION 1: hides tooltip when button is Disabled
elseif k == v.currselector then -- button is focused
+
if v.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
 +
v.ttiptext:setText("("..v.params.tooltiptext..")") -- extra!
 +
if k == v.currselector then v.ttiptext:setVisible(true)
 +
else v.ttiptext:setVisible(false)
 +
end
 +
end
 +
elseif k == v.currselector then -- downState
 
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)
 
1.0, v.params.pixelcolordown, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
vtooltip(k, v)
+
if v.ttiptext then
else -- button is not focused
+
v.ttiptext:setText(v.params.tooltiptext)
 +
if v.tooltiplayer then -- reset tooltip text position
 +
v.ttiptext:setPosition(
 +
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety)
 +
else
 +
v.ttiptext:setPosition(v:globalToLocal(
 +
v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety))
 +
end
 +
v.ttiptext:setVisible(true)
 +
end
 +
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)
 
0.0, v.params.pixelcolorup, v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
vtooltip(k, v)
+
if v.ttiptext then v.ttiptext:setVisible(false) end
 
end
 
end
 
end
 
end
elseif self.ismouse then
+
else
-- print("MOUSE NAVIGATION")
+
-- print("TOUCH, MOUSE NAVIGATION")
self.ismouse = false
+
if self.disabled then -- disabledState
if self.disabled then -- button is Disabled
 
 
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,
Line 252: Line 246:
 
-- if self.ttiptext and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
 
-- if self.ttiptext and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
 
if self.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
 
if self.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
if self.disabled then self.ttiptext:setText("("..self.params.tooltiptext..")") -- extra!
+
self.ttiptext:setText("("..self.params.tooltiptext..")") -- extra!
else self.ttiptext:setText(self.params.tooltiptext)
+
if self.focus then self.ttiptext:setVisible(true)
end
 
if self.hovered then self.ttiptext:setVisible(true)
 
 
else self.ttiptext:setVisible(false)
 
else self.ttiptext:setVisible(false)
 
end
 
end
 
end
 
end
elseif self.hovered then -- button is focused
+
elseif self.focus or self.hover then -- downState
 
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)
 
1.0, self.params.pixelcolordown, self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
if self.ttiptext then self.ttiptext:setVisible(true) end
+
if self.ttiptext then
else -- button is not focused
+
self.ttiptext:setText(self.params.tooltiptext)
 +
self.ttiptext:setVisible(true)
 +
end
 +
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,
Line 273: Line 268:
 
end
 
end
  
-- disabled
+
-- DISABLED
 
function ButtonMonster:setDisabled(disabled)
 
function ButtonMonster:setDisabled(disabled)
 
if self.disabled == disabled then return end
 
if self.disabled == disabled then return end
Line 283: Line 278:
 
end
 
end
  
-- MOUSE LISTENERS
+
-- LISTENERS
function ButtonMonster:onMouseDown(ev)
+
-- MOUSE
if self.sprite:hitTestPoint(ev.x, ev.y, true) then
+
function ButtonMonster:onMouseDown(ev) -- both mouse and touch
-- you can dispatch mouse click event here
+
if self:hitTestPoint(ev.x, ev.y, true) then
 +
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
 +
if self.ttiptext then -- set tooltip initial position
 +
if self.tooltiplayer then
 +
self.ttiptext:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptext: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.ttiptext then -- tooltip follows position
 +
if self.tooltiplayer then
 +
self.ttiptext:setPosition(
 +
ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
 +
else
 +
self.ttiptext: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
 +
self.focus = false
 +
self:updateVisualState()
 +
end
 
ev:stopPropagation()
 
ev:stopPropagation()
 +
elseif self.ttiptext then -- reset tooltip text position
 +
if self.tooltiplayer then
 +
self.ttiptext:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptext:setPosition(self:globalToLocal(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 +
end
 
end
 
end
 
end
 
end
function ButtonMonster:onMouseUp(ev)
+
function ButtonMonster:onMouseUp(ev) -- both mouse and touch
if self.sprite:hitTestPoint(ev.x, ev.y, true) then
+
if self.focus then
 +
self.focus = false
 +
self:updateVisualState()
 
local e = Event.new("clicked")
 
local e = Event.new("clicked")
 
e.currselector = self.selector -- update button id selector
 
e.currselector = self.selector -- update button id selector
Line 299: Line 337:
 
end
 
end
 
end
 
end
function ButtonMonster:onMouseHover(ev)
+
function ButtonMonster:onMouseHover(ev) -- mouse only
if self.sprite:hitTestPoint(ev.x, ev.y, true) then -- onenter
+
if self:hitTestPoint(ev.x, ev.y, true) then -- onenter
self.hovered = true
+
self.focus = true
self.ismouse = true
+
self.hover = true
 
if self.ttiptext then -- tooltip follows mouse position
 
if self.ttiptext then -- tooltip follows mouse position
 
if self.tooltiplayer then
 
if self.tooltiplayer then
Line 308: Line 346:
 
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.sprite:globalToLocal(
+
self.ttiptext: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 319: Line 357:
 
for k, v in ipairs(self.btns) do v.currselector = self.selector end
 
for k, v in ipairs(self.btns) do v.currselector = self.selector end
 
end
 
end
local e = Event.new("hovered") -- button is hovered, dispatch "hovered" event
+
local e = Event.new("hovered") -- dispatch "hovered" event
 
e.currselector = self.selector -- update button id selector
 
e.currselector = self.selector -- update button id selector
 +
e.disabled = self.disabled -- update button disabled
 
self:dispatchEvent(e)
 
self:dispatchEvent(e)
self:selectionSfx() -- play sound fx
+
-- 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
Line 332: Line 371:
 
ev:stopPropagation()
 
ev:stopPropagation()
 
else -- onexit
 
else -- onexit
self.hovered = false
+
self.focus = false
 +
self.hover = false
 
self.onenter = false
 
self.onenter = false
 
self.moving = false
 
self.moving = false
Line 343: Line 383:
 
end
 
end
  
-- audio
+
-- 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.ttiptext then -- reset tooltip text position
 +
if self.tooltiplayer then
 +
self.ttiptext:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptext: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.ttiptext then -- reset tooltip text position
 +
if self.tooltiplayer then
 +
self.ttiptext:setPosition(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
 +
else
 +
self.ttiptext:setPosition(self:globalToLocal(
 +
self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety))
 +
end
 +
end
 +
end
 +
 
 +
-- AUDIO
 
function ButtonMonster:selectionSfx()
 
function ButtonMonster:selectionSfx()
 
if self.params.sound then
 
if self.params.sound then
Line 359: Line 441:
 
=== ButtonMonster '''Demos''' ===
 
=== ButtonMonster '''Demos''' ===
 
'''Demo 1'''
 
'''Demo 1'''
 +
 +
{{#widget:GApp|app=ButtonMonsterDemo1x.GApp|width=480|height=320}}
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
-- DEMO 1: keyboard and mouse navigation
+
-- ButtonMonster DEMO 1
-- bg
+
application:setBackgroundColor(0x6c6c6c)
application:setBackgroundColor(0x00007f)
 
  
-- initial button selected
+
-- font
local selector = 1
+
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 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
 
-- buttons
local btn01 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 1)
+
local btn01 = ButtonMonster.new({
local btn02 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 2)
+
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
local btn03 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 3)
+
text="button 1", ttf=myttf,
local btn04 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 4)
+
sound=btnsound, volume=volume,
-- keyboard navigation
+
}, 1)
local btns = {}
+
local btn02 = ButtonMonster.new({
btns[#btns + 1] = btn01
+
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
btns[#btns + 1] = btn02
+
text="button 2", ttf=myttf,
btns[#btns + 1] = btn03
+
sound=btnsound, volume=volume,
btns[#btns + 1] = btn04
+
}, 2)
 +
local btn03 = ButtonMonster.new({
 +
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
text="button 3", ttf=myttf,
 +
sound=btnsound, volume=volume,
 +
}, 3)
 +
local btn04 = ButtonMonster.new({
 +
autoscale=false,
 +
pixelwidth=8*5, pixelheight=8*24,
 +
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 +
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
 
-- position
 
btn01:setPosition(16*5, 16*3)
 
btn01:setPosition(16*5, 16*3)
btn02:setPosition(16*5, 16*6)
+
btn02:setPosition(16*5, 16*8)
btn03:setPosition(16*5, 16*9)
+
btn03:setPosition(16*5, 16*12.5)
btn04:setPosition(16*5, 16*12)
+
btn04:setPosition(16*12, 16*8)
 +
btnexit:setPosition(16*24, 16*16)
 
-- order
 
-- order
 
stage:addChild(btn01)
 
stage:addChild(btn01)
Line 387: Line 496:
 
stage:addChild(btn03)
 
stage:addChild(btn03)
 
stage:addChild(btn04)
 
stage:addChild(btn04)
 +
stage:addChild(btnexit)
  
-- listener function
+
-- add listeners
function clicked(input, btn)
+
function clicked(btn)
print(input .. " button " .. btn.currselector .. " clicked")
+
print(btn.currselector, btn.disabled)
end
+
if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled())
-- listeners
+
elseif btn.currselector == 5 then
for k, v in ipairs(btns) do
+
if not application:isPlayerMode() then application:exit()
v:addEventListener("clicked", clicked, "mouse", v)
+
else print("EXIT")
v:addEventListener("hovered", function(e) selector = e.currselector end) -- update button id selector
+
end
v.btns = btns -- list of navigatable buttons
 
end
 
 
 
-- keyboard handler (arrows + ENTER)
 
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)
 
function updateButton()
 
for k, v in ipairs(btns) do
 
v.currselector = selector
 
v:updateVisualState()
 
if k == selector then v:selectionSfx() end
 
 
end
 
end
 
end
 
end
 
+
btn01:addEventListener("clicked", clicked)
-- let's go!
+
btn02:addEventListener("clicked", clicked)
updateButton() -- highlight first button
+
btn03:addEventListener("clicked", clicked)
 +
btn04:addEventListener("clicked", clicked)
 +
btnexit:addEventListener("clicked", clicked)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
'''Demo 2'''
 
'''Demo 2'''
  
{{#widget:GApp|app=ButtonMonsterDemo2.GApp|width=480|height=320}}
+
{{#widget:GApp|app=ButtonMonsterDemo2x.GApp|width=480|height=320}}
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
-- DEMO 2
+
-- ButtonMonster DEMO 2: keyboard navigation (arrow keys + ENTER)
 +
application:setBackgroundColor(0x6c6c6c)
 
-- a gradient bg
 
-- a gradient bg
 
local gradient = Pixel.new(0xffffff, 1, application:getContentWidth(), application:getContentHeight())
 
local gradient = Pixel.new(0xffffff, 1, application:getContentWidth(), application:getContentHeight())
Line 448: Line 543:
 
local btn01 = ButtonMonster.new({
 
local btn01 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
text="btn 1", ttf=myttf,
+
text="button 1", ttf=myttf,
tooltiptext="btn1", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*5,
+
tooltiptext="btn1", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 1, tooltiplayer)
 
}, 1, tooltiplayer)
 
local btn02 = ButtonMonster.new({
 
local btn02 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
text="btn 2", ttf=myttf,
+
text="button 2", ttf=myttf,
tooltiptext="click me!", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*1, tooltipoffsety=8*3,
+
tooltiptext="click me!", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 2, tooltiplayer)
 
}, 2, tooltiplayer)
 
local btn03 = ButtonMonster.new({
 
local btn03 = ButtonMonster.new({
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
text="btn 3", ttf=myttf,
+
text="button 3", ttf=myttf,
tooltiptext="btn3", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*5,
+
tooltiptext="btn3", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-1, tooltipoffsety=8*3,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 3, tooltiplayer)
 
}, 3, tooltiplayer)
 
local btn04 = ButtonMonster.new({
 
local btn04 = ButtonMonster.new({
 
autoscale=false,
 
autoscale=false,
pixelwidth=8*3, pixelheight=8*24,
+
pixelwidth=8*5, pixelheight=8*24,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
 
pixelcolordown=0x00ff00,
 
pixelcolordown=0x00ff00,
text="b\nt\nn\n4", ttf=myttf,
+
text="b\nt\nn\n \n4", ttf=myttf,
tooltiptext="btn4", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*4,
+
tooltiptext="btn4", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*-4, tooltipoffsety=8*12,
 
sound=btnsound, volume=volume,
 
sound=btnsound, volume=volume,
 
}, 4, tooltiplayer)
 
}, 4, tooltiplayer)
 +
local btnexit = ButtonMonster.new({
 +
pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
 +
text="EXIT", ttf=myttf,
 +
}, 5, tooltiplayer)
 
-- keyboard navigation
 
-- keyboard navigation
 
local btns = {}
 
local btns = {}
Line 479: Line 578:
 
btns[#btns + 1] = btn03
 
btns[#btns + 1] = btn03
 
btns[#btns + 1] = btn04
 
btns[#btns + 1] = btn04
 +
btns[#btns + 1] = btnexit
 
-- position
 
-- position
 
gradient:setPosition(application:getContentWidth()/2, application:getContentHeight()/2)
 
gradient:setPosition(application:getContentWidth()/2, application:getContentHeight()/2)
Line 485: Line 585:
 
btn03:setPosition(16*5, 16*12.5)
 
btn03:setPosition(16*5, 16*12.5)
 
btn04:setPosition(16*12, 16*8)
 
btn04:setPosition(16*12, 16*8)
 +
btnexit:setPosition(16*24, 16*16)
 
-- order
 
-- order
 
stage:addChild(gradient)
 
stage:addChild(gradient)
stage:addChild(btn01)
+
for k, v in ipairs(btns) do
stage:addChild(btn02)
+
stage:addChild(v)
stage:addChild(btn03)
+
end
stage:addChild(btn04)
 
 
stage:addChild(tooltiplayer)
 
stage:addChild(tooltiplayer)
  
 
-- shared listener functions
 
-- shared listener functions
 
function clicked(input, btn)
 
function clicked(input, btn)
 +
selector = btn.currselector
 
print(input, btn.currselector, btn.disabled)
 
print(input, btn.currselector, btn.disabled)
if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled()) end
+
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
 
end
 
-- add listeners
 
-- add listeners
 
for k, v in ipairs(btns) do
 
for k, v in ipairs(btns) do
 
v:addEventListener("clicked", clicked, "mouse", v)
 
v:addEventListener("clicked", clicked, "mouse", v)
v:addEventListener("hovered", function(e) selector = e.currselector end) -- update button id selector
+
v:addEventListener("hovered", function(e) selector = e.currselector end)
 
v.btns = btns -- list of navigatable buttons
 
v.btns = btns -- list of navigatable buttons
 
end
 
end
  
 
-- keyboard handler
 
-- 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)
 
stage:addEventListener(Event.KEY_DOWN, function(e)
 
if e.keyCode == KeyCode.UP or e.keyCode == KeyCode.LEFT then
 
if e.keyCode == KeyCode.UP or e.keyCode == KeyCode.LEFT then
Line 515: Line 628:
 
end
 
end
 
end)
 
end)
function updateButton()
 
for k, v in ipairs(btns) do
 
v.currselector = selector
 
v:updateVisualState()
 
if k == selector then v:selectionSfx() end
 
end
 
end
 
  
 
-- let's go!
 
-- let's go!

Revision as of 22:37, 29 November 2023

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.

Note: needs Gideros luashader Library

ButtonMonster Class

--[[
-- ButtonMonster
-- Pixel, Image, 9patch, Text, Tooltip,
-- Up, Down, Disabled, Hover,
-- Sfx, Touch, Mouse and Keyboard navigation!
v 0.2.0: 2023-11-29 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)
]]

-- 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
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 8*3 -- 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 8 -- 0, 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 nil -- sound volume
	-- warnings, errors?
	if self.params.pixelalphaup <= 0 then self.params.pixelalphaup = 0.01 end -- alpha <= 0 breaks shader!
	if self.params.pixelalphadown <= 0 then self.params.pixelalphadown = 0.01 end -- alpha <= 0 breaks shader!
	-- 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)
	-- touches 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
		self.text = TextField.new(self.params.ttf, self.params.text, self.params.text)
		self.text:setAnchorPoint(0.5, 0.5)
		self.text:setScale(self.params.textscalexup, self.params.textscaleyup)
		self.text:setTextColor(self.params.textcolorup)
		self.text:setAlpha(self.params.textalphaup)
		textwidth, textheight = self.text:getWidth(), self.text:getHeight()
	end
	-- first add pixel
	local pixelimg = false
	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
	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
	self.pixel:setScale(self.params.pixelscalexup, self.params.pixelscaleyup)
	self.pixel:setAnchorPoint(0.5, 0.5)
	if pixelimg then self.pixel:setShader(shaderpixelslot) end -- apply shader
	self:addChild(self.pixel)
	-- then add text?
	if self.params.text then self:addChild(self.text) end
	-- finally add tooltip?
	if self.params.tooltiptext then
		self.ttiptext = TextField.new(self.params.tooltipttf, self.params.tooltiptext, self.params.tooltiptext)
		self.ttiptext:setScale(self.params.tooltiptextscale)
		self.ttiptext:setTextColor(self.params.tooltiptextcolor)
		self.ttiptext:setVisible(false)
		if self.tooltiplayer then self.tooltiplayer:addChild(self.ttiptext)
		else self:addChild(self.ttiptext)
		end
	end
end

function ButtonMonster:updateVisualState()
	local function visualState(btn, btnscalex, btnscaley, btnalpha, textcolor, textscalex, textscaley,
			pixtexslot, pixelcolor, pixelalpha, pixelscalex, pixelscaley)
		btn:setScale(btnscalex, btnscaley)
		btn:setAlpha(btnalpha)
		if btn.params.text then
			btn.text:setTextColor(textcolor)
			btn.text: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
		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,
					2.0, v.params.pixelcolordisabled, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
--				if v.ttiptext and not v.disabled then -- OPTION 1: hides tooltip when button is Disabled
				if v.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
					v.ttiptext:setText("("..v.params.tooltiptext..")") -- extra!
					if k == v.currselector then v.ttiptext:setVisible(true)
					else v.ttiptext: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,
					1.0, v.params.pixelcolordown, v.params.pixelalphadown, v.params.pixelscalexdown, v.params.pixelscaleydown)
				if v.ttiptext then
					v.ttiptext:setText(v.params.tooltiptext)
					if v.tooltiplayer then -- reset tooltip text position
						v.ttiptext:setPosition(
							v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety)
					else
						v.ttiptext:setPosition(v:globalToLocal(
							v:getX()+v.params.tooltipoffsetx, v:getY()+v.params.tooltipoffsety))
					end
					v.ttiptext: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,
					0.0, v.params.pixelcolorup, v.params.pixelalphaup, v.params.pixelscalexup, v.params.pixelscaleyup)
				if v.ttiptext then v.ttiptext: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,
				2.0, self.params.pixelcolordisabled, self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
--			if self.ttiptext and not self.disabled then -- OPTION 1: hides tooltip when button is Disabled
			if self.ttiptext then -- OPTION 2: shows tooltip even if button is Disabled, you choose!
				self.ttiptext:setText("("..self.params.tooltiptext..")") -- extra!
				if self.focus then self.ttiptext:setVisible(true)
				else self.ttiptext: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,
				1.0, self.params.pixelcolordown, self.params.pixelalphadown, self.params.pixelscalexdown, self.params.pixelscaleydown)
			if self.ttiptext then
				self.ttiptext:setText(self.params.tooltiptext)
				self.ttiptext: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,
				0.0, self.params.pixelcolorup, self.params.pixelalphaup, self.params.pixelscalexup, self.params.pixelscaleyup)
			if self.ttiptext then self.ttiptext:setVisible(false) end
		end
	end
end

-- DISABLED
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
		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
		if self.ttiptext then -- set tooltip initial position
			if self.tooltiplayer then
				self.ttiptext:setPosition(
					self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
			else
				self.ttiptext: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.ttiptext then -- tooltip follows position
			if self.tooltiplayer then
				self.ttiptext:setPosition(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
			else
				self.ttiptext: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
			self.focus = false
			self:updateVisualState()
		end
		ev:stopPropagation()
	elseif self.ttiptext then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptext:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptext: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
		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.ttiptext then -- tooltip follows mouse position
			if self.tooltiplayer then
				self.ttiptext:setPosition(
					ev.x + self.params.tooltipoffsetx, ev.y + self.params.tooltipoffsety)
			else
				self.ttiptext: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
			timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
			timer:start()
		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.ttiptext then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptext:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptext: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.ttiptext then -- reset tooltip text position
		if self.tooltiplayer then
			self.ttiptext:setPosition(
				self:getX()+self.params.tooltipoffsetx, self:getY()+self.params.tooltipoffsety)
		else
			self.ttiptext: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
			snd.sound:play():setVolume(self.params.volume)
			snd.time = curr
		end
	end
end

ButtonMonster Demos

Demo 1

-- ButtonMonster DEMO 1
application:setBackgroundColor(0x6c6c6c)

-- 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 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,
	text="button 1", ttf=myttf,
	sound=btnsound, volume=volume,
}, 1)
local btn02 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 2", ttf=myttf,
	sound=btnsound, volume=volume,
}, 2)
local btn03 = ButtonMonster.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 3", ttf=myttf,
	sound=btnsound, volume=volume,
}, 3)
local btn04 = ButtonMonster.new({
	autoscale=false,
	pixelwidth=8*5, pixelheight=8*24,
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	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

-- 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,
	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