ButtonMonster class
From GiderosMobile
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
-- A Button class with:
-- a Pixel, Image 9patch, Text, Tooltip,
-- Up state, Down state, Disabled state,
-- Hover, Sfx, Mouse and Keyboard navigation!
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.0.1: 2020-03-28 init (based on the initial generic Gideros Button class)
]]
-- Shader, please adapt the path!
--!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!
-- button sprite holder
self.sprite = Sprite.new()
self:addChild(self.sprite)
-- let's go!
self:setButton()
-- update visual state
self:updateVisualState()
self.hovered = nil
self.disabled = nil
self.ismouse = true
-- event listeners
self:addEventListener(Event.MOUSE_DOWN, self.onMouseDown, self)
self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
self:addEventListener(Event.MOUSE_HOVER, self.onMouseHover, 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)
pixelimg = true
end
if self.params.pixelimgdown then
self.pixel:setTexture(self.params.pixelimgdown, 1)
pixelimg = true
end
if self.params.pixelimgdisabled then
self.pixel:setTexture(self.params.pixelimgdisabled, 2)
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
self.sprite:addChild(self.pixel)
-- then add text?
if self.params.text then self.sprite: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
-- VISUAL STATE
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
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
if self.btns then
-- print("KEYBOARD NAVIGATION")
for k, v in ipairs(self.btns) do
if v.disabled then -- button is Disabled
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)
vtooltip(k, v)
elseif k == v.currselector then -- button is focused
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)
vtooltip(k, v)
else -- button is not focused
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)
vtooltip(k, v)
end
end
elseif self.ismouse then
-- print("MOUSE NAVIGATION")
self.ismouse = false
if self.disabled then -- button is Disabled
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!
if self.disabled then self.ttiptext:setText("("..self.params.tooltiptext..")") -- extra!
else self.ttiptext:setText(self.params.tooltiptext)
end
if self.hovered then self.ttiptext:setVisible(true)
else self.ttiptext:setVisible(false)
end
end
elseif self.hovered then -- button is focused
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:setVisible(true) end
else -- button is not focused
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
-- MOUSE LISTENERS
function ButtonMonster:onMouseDown(ev)
if self.sprite:hitTestPoint(ev.x, ev.y, true) then
-- you can dispatch mouse click event here
ev:stopPropagation()
end
end
function ButtonMonster:onMouseUp(ev)
if self.sprite:hitTestPoint(ev.x, ev.y, true) then
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)
if self.sprite:hitTestPoint(ev.x, ev.y, true) then -- onenter
self.hovered = true
self.ismouse = 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.sprite: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") -- button is hovered, dispatch "hovered" event
e.currselector = self.selector -- update button id selector
self:dispatchEvent(e)
self:selectionSfx() -- play sound fx
-- 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.hovered = false
self.onenter = false
self.moving = false
if self.onexit then
-- execute onexit code only once
self.onexit = false
self:updateVisualState()
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
-- DEMO 1: keyboard and mouse navigation
-- bg
application:setBackgroundColor(0x00007f)
-- initial button selected
local selector = 1
-- buttons
local btn01 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 1)
local btn02 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 2)
local btn03 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 3)
local btn04 = ButtonMonster.new({pixelcolorup=0xFF0099, pixelcolordown=0xE300FF,}, 4)
-- keyboard navigation
local btns = {}
btns[#btns + 1] = btn01
btns[#btns + 1] = btn02
btns[#btns + 1] = btn03
btns[#btns + 1] = btn04
-- position
btn01:setPosition(16*5, 16*3)
btn02:setPosition(16*5, 16*6)
btn03:setPosition(16*5, 16*9)
btn04:setPosition(16*5, 16*12)
-- order
stage:addChild(btn01)
stage:addChild(btn02)
stage:addChild(btn03)
stage:addChild(btn04)
-- listener function
function clicked(input, btn)
print(input .. " button " .. btn.currselector .. " clicked")
end
-- listeners
for k, v in ipairs(btns) do
v:addEventListener("clicked", clicked, "mouse", v)
v:addEventListener("hovered", function(e) selector = e.currselector end) -- update button id selector
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
-- let's go!
updateButton() -- highlight first button
Demo 2
-- DEMO 2
-- 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="btn 1", ttf=myttf,
tooltiptext="btn1", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*5,
sound=btnsound, volume=volume,
}, 1, tooltiplayer)
local btn02 = ButtonMonster.new({
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
text="btn 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="btn 3", ttf=myttf,
tooltiptext="btn3", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*5,
sound=btnsound, volume=volume,
}, 3, tooltiplayer)
local btn04 = ButtonMonster.new({
autoscale=false,
pixelwidth=8*3, pixelheight=8*24,
pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
pixelcolordown=0x00ff00,
text="b\nt\nn\n4", ttf=myttf,
tooltiptext="btn4", tooltipttf=myttipttf, tooltiptextcolor=0xaaff00, tooltipoffsetx=8*4,
sound=btnsound, volume=volume,
}, 4, tooltiplayer)
-- keyboard navigation
local btns = {}
btns[#btns + 1] = btn01
btns[#btns + 1] = btn02
btns[#btns + 1] = btn03
btns[#btns + 1] = btn04
-- 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)
-- order
stage:addChild(gradient)
stage:addChild(btn01)
stage:addChild(btn02)
stage:addChild(btn03)
stage:addChild(btn04)
stage:addChild(tooltiplayer)
-- shared listener functions
function clicked(input, btn)
print(input, btn.currselector, btn.disabled)
if btn.currselector == 2 then btn03:setDisabled(not btn03:getDisabled()) 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) -- update button id selector
v.btns = btns -- list of navigatable buttons
end
-- keyboard handler
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
-- let's go!
updateButton() -- highlight first button