ButtonBeast class

From GiderosMobile
Revision as of 00:34, 30 November 2023 by MoKaLux (talk | contribs) (Created page with "__TOC__ '''Requirements''': '''You need to include ''luashader'' standard library in your project''' '''''luashader'' standard library is available in your Gideros install...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Requirements:
You need to include luashader standard library in your project
luashader standard library is available in your Gideros installation folder under Library

ButtonBeast Class

--[[
-- ButtonBeast
-- Pixel, Image, 9patch, Text,
-- Up, Down, Disabled, Hover,
-- Sfx, Touch, Mouse
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
ButtonBeast = Core.class(Sprite)

function ButtonBeast:init(xparams)
	-- user params
	self.params = xparams or {}
	-- 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
	-- 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 ButtonBeast: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
end

function ButtonBeast: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
--	print("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)
	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)
	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)
	end
end

-- DISABLED
function ButtonBeast:setDisabled(disabled)
	if self.disabled == disabled then return end
	self.disabled = disabled
	self:updateVisualState()
end
function ButtonBeast:getDisabled()
	return self.disabled
end

-- LISTENERS
-- MOUSE
function ButtonBeast:onMouseDown(ev) -- both mouse and touch
	if self:hitTestPoint(ev.x, ev.y, true) then
		self.focus = true
		self:updateVisualState()
		self:selectionSfx() -- play sound fx
		ev:stopPropagation()
	end
end
function ButtonBeast:onMouseMove(ev) -- both mouse and touch
	if self.focus then
		if not self:hitTestPoint(ev.x, ev.y, true) then
			self.focus = false
			self:updateVisualState()
		end
		ev:stopPropagation()
	end
end
function ButtonBeast:onMouseUp(ev) -- both mouse and touch
	if self.focus then
		self.focus = false
		self:updateVisualState()
		local e = Event.new("clicked")
		e.disabled = self.disabled -- update button disabled
		self:dispatchEvent(e) -- button is clicked, dispatch "clicked" event
		ev:stopPropagation()
	end
end
function ButtonBeast:onMouseHover(ev) -- mouse only
	if self:hitTestPoint(ev.x, ev.y, true) then -- onenter
		self.focus = true
		self.hover = true
		-- execute onenter code only once
		self.onenter = not self.onenter
		if not self.onenter then self.moving = true end
		if not self.moving then
--			self:selectionSfx() -- play sound fx? you choose!
			-- trick to remove residuals when fast moving mouse
			local timer = Timer.new(100*1, 2) -- number of repetition, the higher the safer
			timer:addEventListener(Event.TIMER, function() self:updateVisualState() end)
			timer:start()
		else
			self.hover = false
			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 ButtonBeast:onTouchesBegin(ev) -- touch only
	if self.focus then ev:stopPropagation() end
end
-- if button is on focus, stop propagation of touch events
function ButtonBeast:onTouchesMove(ev) -- touch only
	if self.focus then ev:stopPropagation() end
end
-- if button is on focus, stop propagation of touch events
function ButtonBeast:onTouchesEnd(ev) -- touch only
	if self.focus then
		ev:stopPropagation()
	end
end
-- if touches are cancelled, reset the state of the button
function ButtonBeast:onTouchesCancel(ev) -- app interrupted (phone call, ...), touch only
	if self.focus then
		self.focus = false
		self:updateVisualState()
	end
end

-- AUDIO
function ButtonBeast: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

ButtonBeast Demo

-- ButtonBeast DEMO
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)
-- 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 = ButtonBeast.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 1", ttf=myttf,
	sound=btnsound, volume=volume,
})
local btn02 = ButtonBeast.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 2", ttf=myttf,
	sound=btnsound, volume=volume,
})
local btn03 = ButtonBeast.new({
	pixelimgup=btnuptex, pixelimgdown=btndowntex, pixelimgdisabled=btndisabledtex,
	text="button 3", ttf=myttf,
	sound=btnsound, volume=volume,
})
local btn04 = ButtonBeast.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,
})
local btnexit = ButtonBeast.new({
	pixelcolorup=0xff0000, pixelcolordown=0x00ff00,
	text="EXIT", ttf=myttf,
})
-- 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)
stage:addChild(btn01)
stage:addChild(btn02)
stage:addChild(btn03)
stage:addChild(btn04)
stage:addChild(btnexit)

-- shared listener functions
function clicked(index, btn)
	print(index, btn.disabled)
	if index == 2 then btn03:setDisabled(not btn03:getDisabled())
	elseif index == 5 then print("EXIT")
	end
end
-- listeners
btn01:addEventListener("clicked", clicked, 1, btn01)
btn02:addEventListener("clicked", clicked, 2, btn02)
btn03:addEventListener("clicked", clicked, 3, btn03)
btn04:addEventListener("clicked", clicked, 4, btn04)
btnexit:addEventListener("clicked", clicked, 5, btnexit)


UI Buttons