ButtonMonster class

From GiderosMobile

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
-- Pixel, Image, 9patch, Text, Tooltip,
-- Up, Down, Disabled, Hover,
-- Sfx, Touch, Mouse and Keyboard navigation!
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 nil -- sound volume
	-- 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
	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.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,
			pixeltex, 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 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.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,
					v.params.pixelimgdown, 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,
					v.params.pixelimgup, 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,
				self.params.pixelimgdisabled, 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,
				self.params.pixelimgdown, 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,
				self.params.pixelimgup, 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
-- 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