Skip to content

Instantly share code, notes, and snippets.

@loopspace
Created May 20, 2013 10:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loopspace/5611552 to your computer and use it in GitHub Desktop.
Save loopspace/5611552 to your computer and use it in GitHub Desktop.
Library UI Release v2.3 -A library of classes and functions for user interface.
Library UI Tab Order
------------------------------
This file should not be included in the Codea project.
#ChangeLog
#Main
#NumberSpinner
#CubicSelector
#PictureBrowser
#Keyboard
#Keypad
#Menu
#Slider
#UI
#PalmRest
#FontPicker
#ListSelector
--[[
ChangeLog
========
v2.3 Picturebrowser runs on start-up
v2.2 Latest version of autogist
v2.1 Updated to use latest version of cmodule. Cleaned up Main
v2.0 Split into separate projects: "Library UI" contains the user
interface elements.
v1.0 Converted to use toadkick's cmodule for importing
and Briarfox's AutoGist for versionning.
It needs toadkick's code from
https://gist.github.com/apendley/5411561
tested with version 0.0.8
To use without AutoGist, comment out the lines in setup.
--]]
--[==[
-- Cubic
local Cubic = class()
local cubicShader
function Cubic:init(...)
self.scale = 200
self.x = WIDTH/2
self.y = HEIGHT/2
self.colour = color(161, 140, 24, 255)
self.bgcolour = color(61, 29, 29, 255)
self.guidecolour = color(28, 101, 139, 129)
local m = mesh()
local nsteps = 200
local w = 5
local h = 1
for n=1,nsteps do
m:addRect(0,(n-.5)*h,w,h)
end
local s = shader()
s.vertexProgram,s.fragmentProgram = cubicShader()
m.shader = s
m.shader.len = nsteps
self.mesh = m
self:setCoefficients(...)
end
function Cubic:setCoefficients(...)
if arg.n == 4 then
self.coefficients = vec4(arg[1],arg[2],arg[3],arg[4])
elseif arg.n == 1 then
if type(arg[1]) == "table" then
self.coefficients = arg[1].coefficients
else
self.coefficients = arg[1]
end
end
self.coefficients = self.coefficients or vec4(0,1,0,0)
self:toBezier()
self:update()
end
function Cubic:update()
self.mesh.shader.pts_1 = self.coefficients.x
self.mesh.shader.pts_2 = self.coefficients.y
self.mesh.shader.pts_3 = self.coefficients.z
self.mesh.shader.pts_4 = self.coefficients.w
end
function Cubic:toBezier()
self.bezier = vec4(
self.coefficients.x,
self.coefficients.y/3,
self.coefficients.x + 2*self.coefficients.y/3
+ self.coefficients.z/3,
self.coefficients.x + self.coefficients.y
+ self.coefficients.z + self.coefficients.w
)
end
function Cubic:fromBezier()
self.coefficients = vec4(
self.bezier.x,
3*(self.bezier.y - self.bezier.x),
3*(self.bezier.x - 2*self.bezier.y + self.bezier.z),
-self.bezier.x + 3*self.bezier.y - 3*self.bezier.z + self.bezier.w
)
self:update()
end
function Cubic:draw()
if not self.active then
return
end
pushMatrix()
resetMatrix()
pushStyle()
resetStyle()
translate(self.x - self.scale/2,self.y - self.scale/2)
scale(self.scale)
fill(self.bgcolour)
RoundedRectangle(-.2,-.2,1.4,1.4,.2)
strokeWidth(5/self.scale)
lineCapMode(SQUARE)
stroke(0, 0, 0, 255)
line(0,0,0,1)
line(0,1,1,1)
line(1,1,1,0)
line(1,0,0,0)
self.mesh:setColors(self.colour)
self.mesh:draw()
stroke(self.guidecolour)
line(0,self.bezier.x,1/3,self.bezier.y)
line(1/3,self.bezier.y,2/3,self.bezier.z)
line(2/3,self.bezier.z,1,self.bezier.w)
popStyle()
popMatrix()
end
function Cubic:isTouchedBy(touch)
return self.active
end
function Cubic:processTouches(g)
if g.updated then
if g.num == 1 then
local t = g.touchesArr[1]
local v = vec2(t.touch.x,t.touch.y)
v = v - vec2(self.x - self.scale/2,self.y - self.scale/2)
v = v / self.scale
if t.touch.state == BEGAN then
if v.x < 1/6 then
self.moving = "x"
elseif v.x < 1/2 then
self.moving = "y"
elseif v.x < 5/6 then
self.moving = "z"
else
self.moving = "w"
end
elseif g.type.moved then
self.bezier[self.moving] = v.y
self:fromBezier()
if self.ctscallback then
self.ctscallback(self.coefficients)
end
end
elseif g.type.tap and g.type.ended then
if self.callback then
if self.callback(self.coefficients) then
self:deactivate()
end
end
elseif g.type.moved then
local v,n = vec2(0,0),0
for _,t in ipairs(g.touchesArr) do
if t.updated then
v = v + vec2(t.touch.deltaX,t.touch.deltaY)
n = n + 1
end
end
if n > 0 then
v = v / n
self.x = self.x + v.x
self.y = self.y + v.y
end
end
g:noted()
end
if g.type.ended and not g.type.tap then
g:reset()
end
end
function Cubic:activate(v,f,ff,b)
self.active = true
self.callback = f
self.ctscallback = ff
if v then
if b then
self.bezier = v
self:fromBezier()
else
self.coefficients = v
self:toBezier()
self:update()
end
end
end
function Cubic:deactivate()
self.active = false
self.callback = nil
self.ctscallback = nil
end
if testsuite then
testsuite.addTest({
name = "Cubic",
setup = function()
local w = Cubic(1,2,3,4)
print(w.coefficients)
local z = Cubic(vec4(2,3,4,5))
print(z.coefficients)
end,
draw = function()
end
})
end
cubicShader = function()
return [[
//
// A basic vertex shader
//
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
uniform float pts_1;
uniform float pts_2;
uniform float pts_3;
uniform float pts_4;
uniform float len;
void main()
{
highp float t = position.y/len;
highp float y = pts_1 + t*pts_2 + t*t*pts_3 + t*t*t*pts_4;
highp float dy = pts_2 + 2.0*t*pts_3 + 3.*t*t*pts_4;
highp vec2 bdir = vec2(dy,-1.);
bdir = position.x*normalize(bdir)/len;
highp vec2 bpos = vec2(t,y) + bdir;
highp vec4 bzpos = vec4(bpos.x,bpos.y,0,1);
//Pass the mesh color to the fragment shader
vColor = color;
vTexCoord = texCoord;
//Multiply the vertex position by our combined transform
gl_Position = modelViewProjection * bzpos;
}
]],[[
//
// A basic fragment shader
//
//This represents the current texture on the mesh
uniform lowp sampler2D texture;
//The interpolated vertex color for this fragment
varying lowp vec4 vColor;
//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;
void main()
{
//Sample the texture at the interpolated coordinate
lowp vec4 col = vColor;
if (vTexCoord.x < .2)
col.a = col.a*vTexCoord.x/.2;
if (vTexCoord.x > .8)
col.a = col.a*(1.-vTexCoord.x)/.2;
//Set the output color to the texture color
gl_FragColor = col;
}
]]
end
return Cubic
--]==]
--[==[
-- FontPicker
local ListSelector = cimport "ListSelector"
local Font,Sentence = unpack(cimport "Font")
local FontPicker = class()
FontPicker.Fonts = {
{"Academy Engraved", "AcademyEngravedLetPlain"},
{"American Typewriter","AmericanTypewriter",
{
{"Light", "AmericanTypewriter-Light"},
{"Regular", "AmericanTypewriter"},
{"Bold", "AmericanTypewriter-Bold"},
{"Condensed Light", "AmericanTypewriter-CondensedLight"},
{"Condensed", "AmericanTypewriter-Condensed"},
{"Condensed Bold", "AmericanTypewriter-CondensedBold"},
}
},
{"Arial","ArialMT",
{
{"Regular","ArialMT"},
{"Italic","Arial-ItalicMT"},
{"Bold","Arial-BoldMT"},
{"Bold Italic","Arial-BoldItalicMT"},
}
},
{"Baskerville","Baskerville",
{
{"Regular","Baskerville"},
{"Italic","Baskerville-Italic"},
{"SemiBold","Baskerville-SemiBold"},
{"SemiBold Italic","Baskerville-SemiBoldItalic"},
{"Bold","Baskerville-Bold"},
{"Bold Italic","Baskerville-BoldItalic"},
}
},
{"Bodoni IT","BodoniSvtyTwoITCTT-Book",
{
{"Regular","BodoniSvtyTwoITCTT-Book"},
{"Italic","BodoniSvtyTwoITCTT-BookIta"},
{"Bold","BodoniSvtyTwoITCTT-Bold"},
{"Small Caps","BodoniSvtyTwoSCITCTT-Book"},
}
},
{"Bodoni OSIT","BodoniSvtyTwoOSITCTT-Book",
{
{"Regular","BodoniSvtyTwoOSITCTT-Book"},
{"Italic","BodoniSvtyTwoOSITCTT-BookIta"},
{"Bold","BodoniSvtyTwoOSITCTT-Bold"},
}
},
--{"Bodoni Ornaments","BodoniOrnamentsITCTT"},
{"Bradley Hand","BradleyHandITCTT-Bold"},
{"Chalkboard","ChalkboardSE-Light",
{
{"Regular","ChalkboardSE-Regular"},
{"Bold","ChalkboardSE-Bold"},
}
},
{"Chalkduster","Chalkduster"},
{"Copperplate","Copperplate",
{
{"Light","Copperplate-Light"},
{"Regular","Copperplate"},
{"Bold","Copperplate-Bold"},
}
},
{"Courier","Courier",
{
{"Regular","Courier"},
{"Oblique","Courier-Oblique"},
{"Bold","Courier-Bold"},
{"Bold Oblique","Courier-BoldOblique"},
}
},
{"Courier New","CourierNewPSMT",
{
{"Regular","CourierNewPSMT"},
{"Bold","CourierNewPS-BoldMT"},
{"Bold Italic","CourierNewPS-BoldItalicMT"},
{"Italic","CourierNewPS-ItalicMT"},
}
},
-- {"DBLCDTempBlack","DBLCDTempBlack"},
{"Didot","Didot",
{
{"Regular","Didot"},
{"Italic","Didot-Italic"},
{"Bold","Didot-Bold"},
}
},
{"Futura","Futura-Medium",
{
{"Medium","Futura-Medium"},
{"Medium Italic","Futura-MediumItalic"},
{"Condensed Medium","Futura-CondensedMedium"},
{"Condensed Extra Bold","Futura-CondensedExtraBold"},
}
},
{"Georgia","Georgia",
{
{"Regular","Georgia"},
{"Italic","Georgia-Italic"},
{"Bold","Georgia-Bold"},
{"Bold Italic","Georgia-BoldItalic"},
}
},
{"Gill Sans", "GillSans",
{
{"Light","GillSans-Light"},
{"Light Italic","GillSans-LightItalic"},
{"Regular","GillSans"},
{"Italic","GillSans-Italic"},
{"Bold","GillSans-Bold"},
{"Bold Italic","GillSans-BoldItalic"},
}
},
{"Helvetica","Helvetica",
{
{"Light","Helvetica-Light"},
{"Light Oblique", "Helvetica-LightOblique"},
{"Regular","Helvetica"},
{"Oblique","Helvetica-Oblique"},
{"Bold","Helvetica-Bold"},
{"Bold Oblique","Helvetica-BoldOblique"},
}
},
{"Helvetica Neue","HelveticaNeue",
{
{"Ultra Light","HelveticaNeue-UltraLight"},
{"Ultra Light Italic", "HelveticaNeue-UltraLightItalic"},
{"Light","HelveticaNeue-Light"},
{"Light Italic","HelveticaNeue-LightItalic"},
{"Regular","HelveticaNeue"},
{"Italic","HelveticaNeue-Italic"},
{"Medium","HelveticaNeue-Medium"},
{"Bold","HelveticaNeue-Bold"},
{"Bold Italic","HelveticaNeue-BoldItalic"},
{"Condensed Bold","HelveticaNeue-CondensedBold"},
{"Condensed Black","HelveticaNeue-CondensedBlack"},
}
},
{"Hoefler","HoeflerText-Regular",
{
{"Regular","HoeflerText-Regular"},
{"Italic","HoeflerText-Italic"},
{"Bold","HoeflerText-Black"},
{"Bold Italic","HoeflerText-BlackItalic"},
}
},
{"Inconsolata","Inconsolata"},
{"Marion","Marion-Regular",
{
{"Regular","Marion-Regular"},
{"Italic","Marion-Italic"},
{"Bold","Marion-Bold"},
}
},
{"MarkerFelt","MarkerFelt-Thin",
{
{"Thin","MarkerFelt-Thin"},
{"Wide","MarkerFelt-Wide"},
}
},
{"Noteworthy","Noteworthy-Light",
{
{"Light","Noteworthy-Light"},
{"Bold","Noteworthy-Bold"},
}
},
{"Optima","Optima-Regular",
{
{"Italic","Optima-Italic"},
{"Regular","Optima-Regular"},
{"Bold","Optima-Bold"},
{"Bold Italic","Optima-BoldItalic"},
{"Extra Bold","Optima-ExtraBlack"},
}
},
{"Palatino","Palatino-Roman",
{
{"Regular","Palatino-Roman"},
{"Italic","Palatino-Italic"},
{"Bold","Palatino-Bold"},
{"Condensed","Palatino-BoldItalic"},
}
},
{"Papyrus","Papyrus",
{
{"Regular","Papyrus"},
{"Condensed","Papyrus-Condensed"},
}
},
{"PartyLetPlain","PartyLetPlain"},
{"Snell Roundhand","SnellRoundhand",
{
{"Regular","SnellRoundhand"},
{"Bold","SnellRoundhand-Bold"},
{"Black","SnellRoundhand-Black"},
}
},
{"Times New Roman","TimesNewRomanPSMT",
{
{"Regular","TimesNewRomanPSMT"},
{"Italic","TimesNewRomanPS-ItalicMT"},
{"Bold","TimesNewRomanPS-BoldMT"},
{"Bold Italic","TimesNewRomanPS-BoldItalicMT"},
}
},
{"Trebuchet","TrebuchetMS",
{
{"Regular","TrebuchetMS"},
{"Italic","TrebuchetMS-Italic"},
{"Bold","TrebuchetMS-Bold"},
{"Bold Italic","Trebuchet-BoldItalic"},
}
},
{"Verdana","Verdana",
{
{"Regular","Verdana"},
{"Italic","Verdana-Italic"},
{"Bold","Verdana-Bold"},
{"Bold Italic","Verdana-BoldItalic"},
}
},
-- "ZapfDingbatsITC", -- Interesting chars not in range 0-255
--]]
--{"Zapfino","Zapfino"}
--]]
}
function FontPicker:init()
end
function FontPicker:activate(t)
t = t or {}
self.args = t
local l = {}
local f = t.fonts or self.Fonts
t.fonts = nil
self.fonts = f
for k,v in ipairs(f) do
table.insert(l,
Sentence(Font({name = v[2], size = 24}), v[1]))
end
t.list = l
self.action = t.action
t.action = nil
self.list = ListSelector(t)
t.list = nil
self.active = true
self.list:activate({
action = function(n) return self:processFont(n) end
})
end
function FontPicker:deactivate()
self.active = false
self.action = nil
self.list:deactivate()
self.list = nil
end
function FontPicker:draw()
if self.active then
self.list:draw()
end
end
function FontPicker:isTouchedBy(touch)
if self.active and self.list then
return self.list:isTouchedBy(touch)
end
return false
end
function FontPicker:processTouches(g)
return self.list:processTouches(g)
end
function FontPicker:processFont(n)
if self.fonts[n][3] then
local t = self.args
t.fonts = self.fonts[n][3]
t.action = self.action
self:activate(t)
else
if self.action(self.fonts[n][2]) then
self:deactivate()
return true
end
end
return false
end
return FontPicker
--]==]
--[==[
-- Keyboard for text input
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
local Keyboard = class()
local Colour = unpack(cimport "Colour",nil)
local UTF8 = cimport "utf8"
local Font,Sentence = unpack(cimport "Font")
cimport "ColourNames"
cimport "RoundedRectangle"
--[[
These global constants define our lock states:
"None": lower case characters
"Cap": next character is capitalised, afterwards revert to "None"
"Shift": upper case characters
"Num": the numerical value of each key is returned.
--]]
local LOCK_NONE = 1
local LOCK_CAP = 2
local LOCK_SHIFT = 3
local LOCK_NUM = 4
function Keyboard:advanceLock()
if self.lock == LOCK_CAP then
if self.locktime and ElapsedTime - self.locktime < 1 then
self.lock = LOCK_SHIFT
else
self.lock = LOCK_NONE
end
elseif self.lock == LOCK_NONE then
self.lock = LOCK_CAP
self.locktime = ElapsedTime
else
self.lock = LOCK_NONE
end
end
Keyboard.qwerty = {
{"q","w","e","r","t","y","u","i","o","p"},
{{nil,nil,nil,nil,.5},"a","s","d","f","g","h","j","k","l"},
{"","z","x","c","v","b","n","m"}
}
Keyboard.fullqwerty = {
{"q","w","e","r","t","y","u","i","o","p",{"DEL",BACKSPACE}},
{{nil,nil,nil,nil,.5},"a","s","d","f","g","h","j","k","l",
{Sentence(Font({
name = "Zapfino", size="100"
}),"CR"),"\n",nil,nil,1.5}},
{{"¥",Keyboard.advanceLock,"£",Keyboard.advanceLock},
"z","x","c","v","b","n","m",{",",nil,"!"},{".",nil,"?"}},
{"","","",{" "," ",nil,nil,6}}
}
function Keyboard:init(t)
self.fontname = t.name
self.fonttype = t.fonttype
self.type = t.type
self.keys = {}
local rl = 0
local nr = 0
self.deactivated = {}
self.keys = {}
local cc
for i,r in ipairs(Keyboard[self.type]) do
local rrl = 0
for j,k in ipairs(r) do
rrl = rrl + 1
end
rl = math.max(rl,rrl)
nr = nr + 1
self.deactivated[i] = {}
end
self.rowlength = rl
self.numrows = nr
self.activekey = {}
self.active = false
self.resize = true
self.colour = Colour.svg.LightGray
self.pcolour = Colour.svg.DarkGray
self.dcolour = Colour.svg.DarkSlateGray
self.kcolour = Colour.svg.Black
self:initialise(t.width)
end
function Keyboard:initialise(w)
w = w or WIDTH
self.iwidth = w
self.keywidth = math.floor(w/self.rowlength)
self.width = self.keywidth * self.rowlength
self.padding = 10
local rl = self.keywidth - 2*self.padding
local f
if self.fonttype == "bitmap" then
local fs = {64,48,32,24,12}
for k,v in ipairs(fs) do
if (v - rl) < 0 then
self.fontsize = v
break
end
end
f = Font[self.fontname .. " " .. self.fontsize .. "0"]()
else
rl = math.floor(rl)
self.fontsize = rl
f = Font({
name = self.fontname,
size = rl
})
end
self.font = f
local row,ccc,CCC,l,key
for i,r in ipairs(Keyboard[self.type]) do
self.keys[i] = {}
for j,k in ipairs(r) do
if type(k) == "string" then
if k ~= "" then
key = UTF8(k)
ccc = Sentence(f,key)
ccc:setColour(self.kcolour)
ccc:prepare()
key:toupper()
CCC = Sentence(f,key)
CCC:setColour(self.kcolour)
CCC:prepare()
self.keys[i][j] = {
ccc,nil,CCC,nil,1
}
else
self.keys[i][j] = {
nil,nil,nil,nil,1
}
end
else
if k[1] then
ccc = Sentence(f,k[1],{"size"})
ccc:setColour(self.kcolour)
ccc:prepare()
if k[3] then
CCC = Sentence(f,k[3],{"size"})
else
CCC = Sentence(f,ccc,{"size"})
CCC:toupper()
end
CCC:setColour(self.kcolour)
CCC:prepare()
l = k[5] or 1
self.keys[i][j] = {
ccc,k[2],CCC,k[4],l
}
else
l = k[5] or 1
self.keys[i][j] = {
nil,nil,nil,nil,l
}
end
end
end
end
self.lh = f:lineheight()
self.keyheight = self.lh + 4*self.padding
local kh = self.keyheight
--[[
local kw = self.keywidth
local kr = self.padding
local image = image(kw,kh)
local hw = math.ceil(kw/2)
local hh = math.ceil(kh/2)
local a,d
for i = 1,hw do
for j = 1,hh do
if i < kr and j < kr then
d = math.sqrt((kr - i)^2 + (kr - j)^2)
elseif i < kr then
d = kr - i
elseif j < kr then
d = kr - j
else
d = 0
end
if d > kr then
a = 0
elseif d > kr - 4 then
a = 255*.25*(kr - d)
else
a = 255
end
image:set(i,j,255,255,255,a)
image:set(kw-i,j,255,255,255,a)
image:set(i,kh-j,255,255,255,a)
image:set(kw-i,kh-j,255,255,255,a)
end
end
self.keypad = image
--]]
self.exh = self.font:exh()
--[[
local xx,yy,xy = 0,0,0
yy = self.numrows * kh
self.height = yy
local row
--]]
self.height = self.numrows * kh
self.lock = LOCK_NONE
self.top = self.height
if displayMode() == FULLSCREEN then
self.top = self.top + 50
end
end
function Keyboard:draw(x,y)
if not self.active then
return
end
x = x or 0
if displayMode() == FULLSCREEN then
y = y or 50
else
y = y or 0
end
self.x = x
self.y = y
pushStyle()
spriteMode(CORNER)
local col,kh,kw,xx,yy,ex,kr
ex = self.exh/2
kh = self.keyheight
kw = self.keywidth
kr = self.padding
yy = y + self.height
self.top = yy
for i,r in ipairs(self.keys) do
yy = yy - kh
xx = x
for j,k in ipairs(r) do
if k[1] then
if self.deactivated[i][j] then
col = self.dcolour
else
if i == self.activekey[1]
and j == self.activekey[2]
then
col = self.pcolour
else
col = self.colour
end
end
fill(col)
RoundedRectangle(xx+kr/8,yy+kr/8,k[5]*kw-kr/4,kh-kr/4,kr)
tint(Colour.svg.Black)
if self.lock == LOCK_NONE then
k[1]:draw(xx+k[5]*kw/2-k[1].width/2,yy+kh/2 - ex)
else
k[3]:draw(xx+k[5]*kw/2-k[3].width/2,yy+kh/2 - ex)
end
end
xx = xx + k[5]*kw
end
end
popStyle()
end
function Keyboard:isTouchedBy(touch)
if not self.active then
return false
end
if touch.x < self.x then
return false
end
if touch.x > self.x + self.width then
return false
end
if touch.y < self.y then
return false
end
if touch.y > self.y + self.height then
return false
end
return true
end
function Keyboard:processTouches(g)
if g.updated then
if g.num == 1 then
local t = g.touchesArr[1]
local x,y,r,c
x = t.touch.x - self.x
y = self.height - t.touch.y + self.y
r = math.floor(y/self.keyheight) + 1
c = x/self.keywidth
for k,v in ipairs(self.keys[r]) do
if c < v[5] then
c = k
break
else
c = c - v[5]
end
end
if not self.deactivated[r][c]
and self.keys[r]
and self.keys[r][c]
and self.keys[r][c][1]
then
self.activekey = {r,c}
end
end
g:noted()
end
if g.type.ended then
if self.activekey[1] then
local k = self.keys[self.activekey[1]][self.activekey[2]]
local kk
if self.lock == LOCK_NONE then
kk = k[2] or k[1]:getUTF8()
else
kk = k[4] or k[3]:getUTF8()
end
if type(kk) == "function" then
kk(self)
else
if self.callback and
self.callback(kk,self.activekey) then
self.active = false
end
if self.lock == LOCK_CAP then
self.lock = LOCK_NONE
end
end
self.activekey = {}
end
g:reset()
end
end
function Keyboard:activate(f)
self.active = true
self.callback = f
self:reactivateallkeys()
end
function Keyboard:deactivate()
self.active = false
self.callback = nil
self:reactivateallkeys()
end
function Keyboard:fullscreen(od,d)
if not self.resize then
return
end
if od == d then
return
end
local ow,w
if d == STANDARD then
w = 751
else
w = 1024
end
if od == STANDARD then
ow = 751
else
ow = 1024
end
self:initialise(self.iwidth * w/ow)
end
function Keyboard:deactivatekey(k)
self.deactivated[k[1]][k[2]] = true
end
function Keyboard:reactivatekey(k)
self.deactivated[k[1]][k[2]] = nil
end
function Keyboard:reactivateallkeys()
for k,v in ipairs(self.deactivated) do
self.deactivated[k] = {}
end
end
return Keyboard
--]==]
--[==[
-- Keypad class for text input
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
--[[
The "Keypad" class defines an object that behaves a little like the
input on a mobile phone. Via a call-back function, it can be used to
get a text string from the user for use in a program.
There are two types of Keypad: the phone type and a numeric type based
on the numeric pad on a keyboard.
--]]
local Keypad = class()
local Font, Sentence = unpack(cimport "Font",nil)
local UTF8 = cimport "utf8"
local Colour = unpack(cimport "Colour",nil)
cimport "ColourNames"
--[[
These global constants define our lock states:
"None": lower case characters
"Cap": next character is capitalised, afterwards revert to "None"
"Shift": upper case characters
"Num": the numerical value of each key is returned.
--]]
local LOCK_NONE = 1
local LOCK_CAP = 2
local LOCK_SHIFT = 3
local LOCK_NUM = 4
local lock_sym = {}
--[[
The locks are indicated on the display by certain strings which need
to be initialised. This needs the "Font" class to be loaded already
so we define a function to do the initialisation.
--]]
local function kp_set_locks(f,c)
lock_sym = {
Sentence(f,"abc"),
Sentence(f,"Abc"),
Sentence(f,"ABC"),
Sentence(f,"123")
}
for k,v in pairs(lock_sym) do
v:setColour(c)
end
end
--[[
The standard keypad has certain features that need to be initialised.
These include:
The characters on the "control" keys.
The characters shown on the keys, both in normal or numeric mode.
The key lists which determine which characters are returned by each
key, both in normal or numeric mode.
--]]
local kp_left
local kp_ret
local kp_right
local kp_del
local kp_keystr
local kp_keylists
local kp_np_keystr
local kp_np_keylists
local kp_initialised
local function kp_initialise()
if kp_initialised then
return true
end
kp_left = UTF8(5589)--utf8hex("ab")
kp_ret = UTF8("Enter")
kp_right = UTF8(5586)
kp_del = UTF8("Del")
kp_keystr = {
{kp_left},
{kp_ret},
{kp_right},
{"1"},
{"2","ABC"},
{"3","DEF"},
{"4","GHI"},
{"5","JKL"},
{"6","MNO"},
{"7","PQRS"},
{"8","TUV"},
{"9","WXYZ"},
{kp_del},
{"0"},
{"^"}
}
kp_keylists = {
{14,{".",",","'","?","!","\"","1","-","(",")","@","/",":","_"},"1"},
{12,{"a","b","c","2","ä","æ","å","à","á","â","ã","ç"},"2"},
{8,{"d","e","f","3","è","é","ê","ë"},"3"},
{8,{"g","h","i","4","ì","í","î","ï"},"4"},
{5,{"j","k","l","5","£"},"5"},
{11,{"m","n","o","6","ö","ø","ò","ó","ô","õ","ñ"},"6"},
{7,{"p","q","r","s","7","ß","$"},"7"},
{8,{"t","u","v","8","ù","ú","û","ü"},"8"},
{5,{"w","x","y","z","9"},"9"},
{1,{""},""},
{2,{" ","0"},"0"}
}
kp_np_keystr = {
{kp_left},
{kp_ret},
{kp_right},
{"7"},
{"8"},
{"9"},
{"4"},
{"5"},
{"6"},
{"1"},
{"2"},
{"3"},
{kp_del},
{"0"},
{"."}
}
kp_np_keylists = {
{1,{},"7"},
{1,{},"8"},
{1,{},"9"},
{1,{},"4"},
{1,{},"5"},
{1,{},"6"},
{1,{},"1"},
{1,{},"2"},
{1,{},"3"},
{1,{},""},
{1,{},"0"},
{1,{},"."}
}
kp_initialised = true
end
--[[
This is our initialisation function which defines the coordinates of
the top left corner, the dimensions of a single "key pad", our colour,
and whether we are numeric or text.
--]]
function Keypad:init(t)
-- you can accept and set parameters here
t = t or {}
kp_initialise()
self.active = false
self.autoactive = true
self.opos = t.pos or function() return 0,0 end
self.anchor = t.anchor or "north west"
self.width = t.width
self.height = t.height
self.colour = t.colour
self.sep = 10
self:orientationChanged()
self.font = t.font or Font({name = "Courier", size = 16})
self.preStr = Sentence(self.font,"")
self.preStr:setColour(Colour.svg.Black)
self.postStr = Sentence(self.font,"")
self.postStr:setColour(Colour.svg.Black)
self.chr = Sentence(self.font,"")
self.chr:setColour(Colour.svg.Black)
self.cursor = Sentence(self.font,UTF8(124))
self.cursor:setColour(Colour.svg.Black)
self.currchr = {}
if t.numeric then
self.keystr = kp_np_keystr
self.keylists = kp_np_keylists
self.keylists[10] = function() self:deleteChar() end
self.lock = LOCK_NUM
else
self.keystr = kp_keystr
self.keylists = kp_keylists
self.lock = LOCK_NONE
self.keylists[10] = function() self:deleteChar() end
self.keylists[12] = function() self:advanceLock() end
kp_set_locks(self.font,Colour.shade(self.colour,50))
end
self.keys = {}
for k,v in ipairs(self.keystr) do
table.insert(self.keys,{})
for l,u in ipairs(v) do
local s = Sentence(self.font,u)
s:setColour(Colour.svg.Black)
table.insert(self.keys[k],s)
end
end
end
function Keypad:orientationChanged()
local x,y = self.opos()
x,y = RectAnchorAt(
x,
y,
3 * (self.width + self.sep),
6 * (self.height + self.sep),
self.anchor)
y = y + 6 * (self.height + self.sep)
self.x = x
self.y = y
end
--[[
This is our draw function which renders the keypad and its constituent
parts to the screen, assuming that we are active.
--]]
function Keypad:draw()
if self.active then
local k,x,y
pushStyle()
smooth()
noStroke()
ellipseMode(RADIUS)
fill(self.colour)
k = 0
for j = 2,6 do
for i = 0,2 do
k = k + 1
x = self.x + i * (self.width + self.sep) + self.sep/2
y = self.y - j * (self.height + self.sep) + self.sep/2
RoundedRectangle(
x,
y,
self.width,
self.height,
self.sep)
if self.keys[k] then
for l,u in ipairs(self.keys[k]) do
u:prepare()
u:draw(
x + self.width/2 - u.width/2,
y + self.height/2 - self.font:lineheight() * (l - 1)
)
end
end
end
end
x = self.x + self.sep/2
y = self.y - self.sep/2 - self.height
RoundedRectangle(
x,
y,
3*self.width + 2*self.sep,
self.height,
self.sep)
self.preStr:prepare()
self.postStr:prepare()
self.chr:prepare()
x = x + self.sep
y = y + (self.height - self.font:lineheight())/2
x,y = self.preStr:draw(x,y)
x,y = self.chr:draw(x,y)
if math.floor(2*ElapsedTime)%2 == 0 then
self.cursor:draw(x,y)
end
x = x + self.font:charWidth()
x,y = self.postStr:draw(x,y)
x = self.x + self.sep
y = self.y - self.sep/2 - self.font:lineheight()
lock_sym[self.lock]:draw(x,y)
popStyle()
end
end
--[[
If we are active then we claim any touch that falls within our natural
"bounding box". If we're in "autoactive" mode then a touch outside our bounding box turns us off.
--]]
function Keypad:isTouchedBy(touch)
if self.active then
if touch.x < self.x
or touch.x > self.x + 3 * (self.width + self.sep)
or touch.y > self.y
or touch.y < self.y - 6 * (self.height + self.sep) then
if self.autoactive then
self:deactivate()
return true
else
return false
end
end
return true
end
end
--[[
This routine process the touches. We process touches one by one as
they end. In normal mode, we have to keep track of the key that was
last pressed since if the same key is pressed within half a second
then we replace the previous character by the next on the list for
that key.
As we can move forwards and backwards in the string, we actually
maintain three Sentence objects: before the cursor, the current
character, and after the cursor. Moving the cursor shifts characters
back and forth between these three.
We also have to keep track of the current lock.
--]]
function Keypad:processTouches(g)
if self.active then
if g.updated then
local t,n,i,j
t = g.touchesArr
table.sort(t,sortByCreated)
for k,v in ipairs(t) do
if v.touch.state == ENDED then
j = math.floor((self.y - v.touch.y)/(self.height + self.sep)) + 1
if j > 1 and j < 7 then
i = math.floor((v.touch.x - self.x)/(self.width + self.sep)) + 1
if j == 2 then
self.preStr:append(self.chr)
self.chr:clear()
self.currchr = {}
if i == 2 then
self.preStr:append(self.postStr)
self.postStr:clear()
if self.returnFn then
local r
r = self.returnFn(self.preStr:getString())
if r then
self:deactivate()
end
end
elseif i == 1 then
self.postStr:unshift(self.preStr:pop())
elseif i == 3 then
self.preStr:push(self.postStr:shift())
end
else
n = i + 3*(j - 3)
if n == self.currchr[1]
and v.createdat - self.currchr[4] < .5 then
self.currchr[2] = self.currchr[2] + 1
self.currchr[4] = v.createdat
elseif type(self.keylists[n]) == "function" then
self.keylists[n]()
else
if self.lock == LOCK_CAP
and self.currchr[1] then
self.lock = LOCK_NONE
end
self.preStr:append(self.chr)
self.chr:clear()
self.currchr = {n,1,self.lock,v.updatedat}
end
end
v:destroy()
if self.currchr[1] then
local c
local a = self.keylists[self.currchr[1]]
if self.currchr[3] == LOCK_NUM then
c = UTF8(a[3])
else
local d = (self.currchr[2] - 1) % a[1] +1
c = UTF8(a[2][d])
end
if self.currchr[3] == LOCK_CAP or
self.currchr[3] == LOCK_SHIFT then
c:toupper()
end
self.chr:setString(c)
end
end
end
end
end
g:noted()
else
g:reset()
end
end
--[[
This functions shifts the lock to the next state, saving the current
character into the before-cursor string.
--]]
function Keypad:advanceLock()
self.preStr:append(self.chr)
self.chr:clear()
self.currchr = {}
self.lock = (self.lock) % 4 + 1
end
--[[
This deletes the current character (which may be the last one on the
before-cursor string).
--]]
function Keypad:deleteChar()
if self.currchr[1] then
self.chr:clear()
self.currchr = {}
else
self.preStr:pop()
end
end
--[[
This activates the keypad, and sets the call-back function for when
the text input is finalised.
--]]
function Keypad:activate(f,x,y)
self:clear()
if x then
self.x = x
end
if y then
self.y = y
end
if f then
self.returnFn = f
end
self.active = true
end
function Keypad:deactivate()
self:clear()
if self.autoactive then
self.active = false
end
end
--[[
This clears all the strings.
--]]
function Keypad:clear()
self.preStr:setString("")
self.postStr:setString("")
self.chr:setString("")
self.currchr = {}
self.returnFn = nil
end
--[[
This is for sorting touches by their start time.
--]]
local function sortByCreated(a,b)
return a.createdat < b.createdat
end
Keypad.help = "The keypad and numeric pad are used to get (short) input from the user. The keypad acts like the input on a phone: multiple taps on the same key on quick succession cycle through the letters, and the case-key acts as on a phone. The numeric pad is similar to the numeric pad on a computer. For both, the upper keys are for left-right navigation through the input and to accept the input. The pad can be cancelled by touching some part of the screen away from it."
return Keypad
--]==]
--[==[
-- List Selector
local Colour = unpack(cimport "Colour",nil)
cimport "ColourNames"
--cimport "Utilities"
local ListSelector = class()
function ListSelector:init(t)
t = t or {}
local list = t.list or {}
local mf = t.miniumSize or 10
self.minsize = mf
self.choice = 1
local w = 0
local nf = 0
local h = 0
for k,s in ipairs(list) do
s:prepare()
w = math.max(w,s.width)
nf = nf + 1
h = math.max(h, s.font:lineheight())
end
w = w + 10
self.totalwidth = w + 2
self.nitems = nf
if nf > mf then
mf = nf
self.restrict = self.restrictMod
end
self.mitems = mf
self.pos = t.pos or function() return RectAnchorOf(Screen,"centre") end
self.ipos = self.pos
self.width = w
local hw = w/2
self.m = mesh()
self.overlay = mesh()
local ver = {}
local col = {}
for k,v in ipairs({
{0,0},
{0,1},
{1,1},
{0,0},
{1,0},
{1,1},
{0,0},
{0,-1},
{1,-1},
{0,0},
{1,0},
{1,-1}
}) do
table.insert(ver,vec2(v[1],v[2]))
table.insert(col,
Colour.opacity(Colour.svg.Black,100*math.abs(v[2])))
end
self.overlay.vertices = ver
self.overlay.colors = col
local img = image(2*w,h*math.ceil(mf/2))
local x,y = 5,h*math.ceil(mf/2)
pushMatrix()
resetMatrix()
pushStyle()
resetStyle()
fill(Colour.svg.White)
noSmooth()
setContext(img)
rect(0,0,2*w,math.ceil(mf/2)*h)
for i=1,nf do
y = y + list[i].font.descent - h
list[i]:draw(x,y)
y = y - list[i].font.descent
if i == math.ceil(mf/2) then
x,y = w+5,h*math.ceil(mf/2)
end
end
setContext()
self.m.texture = img
self.img = img
local nv = 2*mf
local st = 2*math.pi/nv
ver = {}
local texc = {}
local a,b,c,d,tx,ty
local r = mf*h/(2*math.pi)
self.radius = r
a = 0
b = -r
tx,ty = 0,0
for i = 1,nv do
c = -r*math.sin(i*st)
d = -r*math.cos(i*st)
for k,v in ipairs({
{-hw,a,b,0,2*(i-1)/nv},
{hw,a,b,.5,2*(i-1)/nv},
{hw,c,d,.5,2*i/nv},
{-hw,a,b,0,2*(i-1)/nv},
{-hw,c,d,0,2*i/nv},
{hw,c,d,.5,2*i/nv}
}) do
table.insert(ver,vec3(v[1],v[2],v[3]))
table.insert(texc,vec2(tx+v[4],v[5]-ty))
end
if i == mf then
tx,ty = .5,2*(i-1)/nv
end
a,b = c,d
end
self.m.vertices = ver
self.m.texCoords = texc
self.m:setColors(Colour.svg.White)
self.velocity = 0
debug:log({name = "List: ", message = function() return self.choice end})
end
function ListSelector:draw()
if self.active then
local ang
local w = self.totalwidth
local r = self.radius
local x,y = self.pos()
pushStyle()
pushMatrix()
resetMatrix()
resetStyle()
ortho(0,WIDTH,0,HEIGHT,2*r,-2*r)
translate(x,y,0)
camera(0,0,2*r,0,0,0,0,1,0)
local v = self.choice
ang = (v -.5)*360/self.mitems
pushMatrix()
rotate(ang,-1,0,0)
self.m:draw()
popMatrix()
if not self.intouch then
self.choice,self.velocity = self:restrict(v,self.velocity)
end
resetMatrix()
stroke(Colour.svg.SlateBlue)
strokeWidth(6)
noFill()
noSmooth()
rectMode(CORNERS)
pushMatrix()
translate(x-self.totalwidth/2-5,y)
scale(self.totalwidth +10,r+2)
self.overlay:draw()
popMatrix()
rect(x-self.totalwidth/2-5,
y-r-5,
x + self.totalwidth/2+5,
y+r+5)
popMatrix()
popStyle()
end
end
function ListSelector:activate(t)
t = t or {}
self.pos = t.pos or self.pos
self.choice = t.value or 1
self.active = true
self.action = t.action or function() return true end
end
function ListSelector:deactivate()
self.pos = self.ipos
self.active = false
self.action = function() return true end
self.subfp = nil
end
function ListSelector:getValue()
local v = math.floor(self.choice+.5)
v = (v-1)%self.nitems + 1
return v
end
function ListSelector:restrict(v,s)
if s == 0 then
if v ~= math.floor(v) then
local tgt = math.max(1,
math.min(self.nitems,math.floor(v + .5)))
if math.abs(v - tgt) < .01 then
v = tgt
else
v = v + DeltaTime*(tgt- v)
end
else
v = (v-1)%self.nitems + 1
end
else
if v < -2 then
s = math.abs(s)/2
end
if v > self.nitems + 2 then
s = - math.abs(s)/2
end
v = v + DeltaTime*s
s = s * .99
if math.abs(s) < .1 then
s = 0
end
end
return v,s
end
function ListSelector:restrictMod(v,s)
if s == 0 then
if v ~= math.floor(v) then
local tgt = math.floor(v + .5)
if math.abs(v - tgt) < .01 then
v = tgt
else
v = v + DeltaTime*(tgt- v)
end
else
v = (v-1)%self.nitems + 1
end
else
v = v + DeltaTime*s
s = s * .99
if math.abs(s) < .1 then
s = 0
end
end
return v,s
end
function ListSelector:isTouchedBy(touch)
if not self.active then
return false
end
local x,y = self.pos()
if touch.x < x - self.totalwidth/2 then
return false
end
if touch.x > x + self.totalwidth/2 then
return false
end
if touch.y < y - self.radius then
return false
end
if touch.y > y + self.radius then
return false
end
self.intouch = true
return true
end
function ListSelector:processTouches(g)
if g.type.ended and g.type.tap and g.num == 2 then
local value = self:getValue()
if self.action and self.action(value) then
self:deactivate()
end
g:reset()
return
end
local x,y = self.pos()
local t = g.touchesArr[1]
if t.touch.state ~= BEGAN then
if t.updated then
self.choice = (self.choice - 1 +
t.touch.deltaY/HEIGHT
*self.radius/self.mitems)%self.mitems
+ 1
end
end
if t.touch.state == ENDED then
if t.short and t.moved then
self.velocity = 2*t:velocity().y/HEIGHT
*self.radius/self.mitems
else
self.velocity = 0
end
end
g:noted()
if (not g.type.tap and g.type.ended) or g.type.finished then
self.intouch = false
g:reset()
end
end
ListSelector.help = "Drag the wheels up and down to change the digits. Drag the outermost wheels to add or remove digits."
return ListSelector
--]==]
VERSION = 2.3
clearProjectData()
-- DEBUG = true
-- Use this function to perform your initial setup
function setup()
autogist = AutoGist("Library UI","A library of classes and functions for user interface.",VERSION)
autogist:backup(true)
--displayMode(FULLSCREEN_NO_BUTTONS)
cmodule "Library UI"
cmodule.path("Library Base", "Library Utilities")
local Touches = cimport "Touch"
local UI = cimport "UI"
local Debug = cimport "Debug"
local Colour = unpack(cimport "Colour",nil)
cimport "ColourNames"
cimport "PictureBrowser"
cimport "Menu"
cimport "Keypad"
cimport "Keyboard"
if cmodule.loaded("Menu") then
print("Menu loaded")
else
print("Menu not loaded")
end
touches = Touches()
ui = UI(touches)
debug = Debug({ui = ui})
ui:systemmenu()
ui:helpmenu()
ui:addMessage("This is a system message, hello everyone.")
debug:log({
name = "Screen north west",
message = function() local x,y = RectAnchorOf(Screen,"north west") return x .. ", " .. y end
})
--debug:activate()
ui:setPictureList({directory = "Documents", camera = true, filter = function(n,w,h) return math.min(w,h) > 500 end})
ui:getPicture(function(i) img = i return true end)
--print(USRotateCCW(vec2(1,0)))
ui:declareKeyboard({name = "ArialMT", type = "fullqwerty"})
ui:useKeyboard("fullqwerty",
function(k) print(k) return false end)
--ui:getNumber(function(n) print(n) end)
end
-- This function gets called once every frame
function draw()
-- process touches and taps
touches:draw()
background(34, 47, 53, 255)
if img then
local w,h = img.width,img.height
local asp = math.min(WIDTH/w,HEIGHT/h,1)
sprite(img,WIDTH/2,HEIGHT/2)
end
ui:draw()
debug:draw()
touches:show()
end
function touched(touch)
touches:addTouch(touch)
end
function orientationChanged(o)
if ui then
ui:orientationChanged(o)
end
end
function fullscreen()
end
function reset()
end
--[==[
-- Menu class
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
--[[
The "Menu" class defines a generic menu object. This contains a list
and items on this list can be selected whereupon some appropriate
function is called. Items can also be displayed as highlighted
according to the return value of some other function.
--]]
local Menu = class()
local Font, Sentence = unpack(cimport "Font",nil)
local Colour = unpack(cimport "Colour",nil)
cimport "ColourNames"
--[[
Our initial data is our position and optional title.
--]]
function Menu:init(t)
t = t or {}
self.opos = t.pos or function() return 0,0 end
self.anchor = t.anchor or "north west"
self:orientationChanged()
self.dir = t.direction or "y"
self.font = t.font or Font({name = "Courier", size = 16})
self.onactive = t.onActivation or function() end
self.ondeactive = t.onDeactivation or function() end
local title = t.title or ""
if title == "" then
self.hasTitle = false
else
self.hasTitle = true
end
self.title = Sentence(self.font,title)
self.sep = 10
self.colour = t.colour or Colour.svg.Bisque
self.textColour = t.textColour or Colour.svg.DarkSlateBlue
self.title:setColour(self.textColour)
self.items = {}
self.nextitem = 1
self.nitems = 0
self.width = 0
self.height = 0
self.highlight = -1
self.itemHeight = self.font:lineheight() + self.sep
self.children = {}
if t.autoactive == nil then
self.autoactive = true
else
self.autoactive = t.autoactive
end
end
function Menu:orientationChanged()
local x,y = self.opos()
self.x = x
self.y = y
end
--[[
The "draw" function calls another draw option depending on whether the
menu goes across or down.
--]]
function Menu:draw()
if self.nitems == 0 then
return
end
if self.dir == "x" then
self:drawAcross()
else
self:drawDown()
end
end
--[[
This is for vertical lists. We have to work out our width first, and
the title is not set to the full width to allow for a "tab like"
effect.
An item may be "highlighted", if it has a suitable function defined
and if the return value of this is true. An item may also be
"selected" (more accurately, "potentially selected") if the current
touch is hovering over it.
--]]
function Menu:drawDown()
pushStyle()
fill(self.colour)
spriteMode(CORNER)
self.title:prepare()
local sep = self.sep
local lh = self.itemHeight
local x = self.x
local y = self.y
local w = 0
local h = 0
local c = 0
local iw,ih
if self.hasTitle then
w = self.title.width
h = lh + sep
end
if self.active then
for k,v in ipairs(self.items) do
v.title:prepare()
if v.title.width > w then
w = v.title.width
end
h = h + lh
end
end
w = w + 2*sep
h = h + sep
self.width = w
self.height = h
x,y = RectAnchorAt(x,y,w,h,self.anchor)
local th = 0
local tw = 0
if self.hasTitle then
tw = self.title.width + 2*sep
th = lh + sep
if self.active then
c = 9
else
c = 0
end
local ty = y + h - th
RoundedRectangle(
x,
ty,
tw,
th,
sep,
c)
self.title:draw(x+sep,ty+sep)
end
h = h - th
if self.active then
if self.hasTitle then
if self.width == tw then
c = 6
else
c = 2
end
else
c = 0
end
RoundedRectangle(
x,
y,
w,
h,
sep,
c
)
x = x + sep
w = w - sep
y = y + h
for k,v in ipairs(self.items) do
y = y - lh
if k == self.highlight and v.action then
fill(Colour.shade(self.colour,50))
RoundedRectangle(
x - sep/2,
y - sep/2,
w,
lh,
sep/2)
elseif v.highlight() then
fill(Colour.tint(self.colour,50))
RoundedRectangle(
x - sep/2,
y - sep/2,
w,
lh,
sep/2)
end
if v.icon then
iw,ih = spriteSize(v.icon)
sprite(v.icon,x,y,(lh-sep)*iw/ih,lh-sep)
else
v.title:draw(x,y)
end
end
end
popStyle()
end
--[[
This is for horizontal lists.
--]]
function Menu:drawAcross()
pushStyle()
noSmooth()
spriteMode(CORNER)
fill(self.colour)
self.title:prepare()
local sep = self.sep
local lh = self.itemHeight
local w = 0
local h = lh + sep
local iw,ih
if self.hasTitle then
w = self.title.width + 2*sep
end
if self.active then
for k,v in ipairs(self.items) do
if v.icon then
iw,ih = spriteSize(v.icon)
w = w + iw*(lh-sep)/ih + sep
else
v.title:prepare()
w = w + v.title.width + sep
end
end
w = w + sep
end
local x,y = RectAnchorAt(
self.x,
self.y,
w,
h,
self.anchor)
if self.active or self.hasTitle then
RoundedRectangle(
x,
y,
w,
h,
sep
)
end
self.width = w
self.height = h
x = x + sep
y = y + sep
if self.active then
for k,v in ipairs(self.items) do
if v.icon then
iw,ih = spriteSize(v.icon)
w = iw*(lh-sep)/ih + sep
else
w = v.title.width + sep
end
if k == self.highlight and v.action then
fill(Colour.shade(self.colour,50))
RoundedRectangle(
x - sep/2,
y - sep/2,
w,
lh,
sep/2)
elseif v.highlight() then
fill(Colour.tint(self.colour,50))
RoundedRectangle(
x - sep/2,
y - sep/2,
w,
lh,
sep/2)
end
if v.icon then
iw,ih = spriteSize(v.icon)
sprite(v.icon,x,y,(lh-sep)*iw/ih,lh-sep)
else
v.title:draw(x,y)
end
x = x + w
end
end
popStyle()
end
--[[
If we are active then we claim all touches in our bounding box. If we
are not active then we still claim touches on our title.
--]]
function Menu:isTouchedBy(touch)
if self.nitems == 0 then
return false
end
if not self.active and not self.hasTitle then
return false
end
local x,y = RectAnchorAt(
self.x,
self.y,
self.width,
self.height,
self.anchor)
if touch.x < x then
return false
end
if touch.x > x + self.width then
return false
end
if touch.y < y then
return false
end
if touch.y > y + self.height then
return false
end
return true
end
--[[
For the touch processing, we need to know which element in the list is
currently under the touch. While the touch is in progress then this
element is highlighted. When the touch ends, the call-back function
for this element is called. The arguments to this call-back are an
appropriate xy coordinate for the element in the list (which can be
used to make something appear alongside or just below the element).
--]]
function Menu:processTouches(g)
if g.updated then
if not self.active then
if g.type.ended then
self:activate()
g:reset()
end
else
local t
t = g.touchesArr
for k,v in ipairs(t) do
if v.touch.state == ENDED then
local n,x,y = self.highlight,self.itemx,self.itemy
if self.items[n] and self.items[n].action then
self:deactivateChildrenExcept(n)
local r = self.items[n].action(x,y)
if r then
self:deactivateUp()
end
end
v:destroy()
self.highlight = -1
else
local n,x,y,w,sep
x,y = RectAnchorAt(
self.x,
self.y,
self.width,
self.height,
self.anchor)
sep = self.sep
if self.dir == "y" then
n = math.floor((y + self.height - sep/2 - v.touch.y)
/self.itemHeight)
if not self.hasTitle then
n = n + 1
y = y + self.itemHeight
end
y = y + self.height - n * self.itemHeight - self.sep/2
x = x + self.width + 1
else
n = 0
--y = y - self.itemHeight - sep
y = y - 1
if self.hasTitle then
if x + self.title.width + sep < v.touch.x then
x = x + self.title.width + sep
end
end
if x < v.touch.x then
n = n + 1
local iw,ih
local lh = self.itemHeight
for l,u in ipairs(self.items) do
if u.icon then
iw,ih = spriteSize(u.icon)
if x + iw*(lh-sep)/ih + sep > v.touch.x then
break
end
x = x + iw*(lh-sep)/ih + sep
else
if x + u.title.width + sep > v.touch.x then
break
end
x = x + u.title.width + sep
end
n = n + 1
end
end
end
self.highlight = n
self.itemx = x
self.itemy = y
end
end
end
end
g:noted()
end
function Menu:invoke(n,x,y)
local m,r
for _,v in ipairs(self.items) do
m = v.title:getString()
if m == n then
r = v.action(x,y)
break
end
end
return r
end
--[[
This adds an item to the menu. The arguments are the text, the
call-back function, the highlighter function, and whether to
increment the "next item" counter (for backwards compatibility, the
argument is whether to not increment).
--]]
function Menu:addItem(t)
t = t or {}
local m = {}
m.title = Sentence(self.font,t.title)
m.title:setColour(self.textColour)
m.action = t.action
m.deselect = t.deselect or function() end
m.highlight = t.highlight or function() return false end
m.icon = t.icon
table.insert(self.items,self.nextitem,m)
if not t.atEnd then
self.nextitem = self.nextitem + 1
end
self.nitems = self.nitems + 1
return m
end
--[[
Try to remove an item corresponding to the given title.
--]]
function Menu:removeItem(t)
for k,v in ipairs(self.items) do
if v.title:getString() == t then
table.remove(self.items,k)
return v
end
end
end
function Menu:deactivate()
if self.autoactive and self.active then
self.active = false
self.ondeactive()
self:deactivateChildren()
self:deactivateParent()
end
end
function Menu:deactivateDown()
if self.autoactive and self.active then
self.active = false
self.ondeactive()
self:deactivateChildren()
end
end
function Menu:deactivateUp()
if self.autoactive and self.active then
self.active = false
self.ondeactive()
self:deactivateParent()
end
end
function Menu:deactivateChildrenExcept(n)
for k,v in ipairs(self.items) do
if k ~= n then
if v.highlight() then
v.deselect()
end
end
end
end
function Menu:deactivateChildren()
for k,v in ipairs(self.items) do
if v.highlight() then
v.deselect()
end
end
end
function Menu:deactivateParent()
if self.parent then
self.parent:deactivate()
end
end
function Menu:activate()
self.active = true
self.onactive()
self:activateParent()
end
function Menu:toggle()
if self.active then
self:deactivate()
else
self:activate()
end
end
function Menu:activateParent()
if self.parent then
self.parent:activate()
end
end
function Menu:isChildOf(m)
self.parent = m
m:addChild(self)
end
function Menu:addChild(m)
table.insert(self.children,m)
end
return Menu
--]==]
--[==[
--NumberSpinner
local NumberSpinner = class()
local Font = unpack(cimport "Font",nil)
local Colour = unpack(cimport "Colour",nil)
cimport "Coordinates"
cimport "Rectangle"
cimport "ColourNames"
function NumberSpinner:init(t)
t = t or {}
self.numbers = {0}
self.numdigits = 1
self.decimals = {}
self.numdecs = 0
self.maxdecs = 5
self.maxdigits = 5
self.allowneg = true
local fnt = t.font or Font({name = "Inconsolata", size = 60})
self.pos = t.pos or function() return RectAnchorOf(Screen,"centre") end
self.ipos = self.pos
local h = fnt:lineheight()
local ds = fnt.descent
local w = fnt:charWidth("0")
fnt:setColour(Colour.svg.Black)
self.width = w
local hw = w/2
self.m = mesh()
self.pm = mesh()
self.dpt = mesh()
self.overlay = mesh()
local ver = {}
local col = {}
for k,v in ipairs({
{0,0},
{0,1},
{1,1},
{0,0},
{1,0},
{1,1},
{0,0},
{0,-1},
{1,-1},
{0,0},
{1,0},
{1,-1}
}) do
table.insert(ver,vec2(v[1],v[2]))
table.insert(col,
Colour.opacity(Colour.svg.Black,100*math.abs(v[2])))
end
self.overlay.vertices = ver
self.overlay.colors = col
local img = image(w,10*h)
pushMatrix()
resetMatrix()
pushStyle()
resetStyle()
fill(Colour.svg.White)
noSmooth()
setContext(img)
rect(0,0,w,10*h)
for i=0,9 do
fnt:write(i,0,i*h+ds)
end
setContext()
self.m.texture = img
img = image(w,10*h)
setContext(img)
rect(0,0,w,10*h)
fnt:write("-",0,4.5*h+ds)
setContext()
self.pm.texture = img
img = image(w,10*h)
setContext(img)
rect(0,0,w,10*h)
fnt:write(".",0,4.75*h+ds)
setContext()
popStyle()
popMatrix()
self.dpt.texture = img
local nv = 40
local st = 2*math.pi/nv
ver = {}
local texc = {}
local a,b,c,d
local r = 5*h/math.pi
self.radius = r
a = 0
b = -r
for i = 1,nv do
c = -r*math.sin(i*st)
d = -r*math.cos(i*st)
for k,v in ipairs({
{-hw,a,b,0,(i-1)/nv},
{hw,a,b,1,(i-1)/nv},
{hw,c,d,1,i/nv},
{-hw,a,b,0,(i-1)/nv},
{-hw,c,d,0,i/nv},
{hw,c,d,1,i/nv}
}) do
table.insert(ver,vec3(v[1],v[2],v[3]))
table.insert(texc,vec2(v[4],v[5]))
end
a,b = c,d
end
self.m.vertices = ver
self.m.texCoords = texc
self.m:setColors(Colour.svg.White)
self.pm.vertices = ver
self.pm.texCoords = texc
self.pm:setColors(Colour.svg.White)
self.dpt.vertices = ver
self.dpt.texCoords = texc
self.dpt:setColors(Colour.svg.White)
end
function NumberSpinner:draw()
if self.active then
local ang,tgt
local w = self.width +2
local r = self.radius
local tw = 0
local x,y = self.pos()
pushStyle()
pushMatrix()
resetMatrix()
resetStyle()
ortho(0,WIDTH,0,HEIGHT,2*r,-2*r)
translate(x+w/2,y,0)
camera(0,0,2*r,0,0,0,0,1,0)
for k,v in ipairs(self.numbers) do
ang = v*36 - 162
tw = tw + w
translate(-w,0,0)
pushMatrix()
rotate(ang,1,0,0)
self.m:draw()
popMatrix()
if not self.intouch and v ~= math.floor(v) then
tgt = math.floor(v + .5)
if math.abs(v - tgt) < .01 then
self.numbers[k] = tgt
else
self.numbers[k] = v + DeltaTime*(tgt- v)*10
end
end
end
self.numwidth = tw
if self.isneg then
translate(-w,0,0)
self.pm:draw()
tw = tw + w
end
self.totallwidth = tw
self.totalwidth = tw
self.decwidth = 0
if self.numdecs > 0 then
translate(tw,0)
self.dpt:draw()
tw = w
for k,v in ipairs(self.decimals) do
ang = v*36 - 162
tw = tw + w
translate(w,0,0)
pushMatrix()
rotate(ang,1,0,0)
self.m:draw()
popMatrix()
if not self.intouch and v ~= math.floor(v) then
tgt = math.floor(v + .5)
if math.abs(v - tgt) < .01 then
self.decimals[k] = tgt
else
self.decimals[k] = v + DeltaTime*(tgt- v)*10
end
end
end
self.decwidth = tw
self.totalwidth = self.totalwidth + tw
end
--ortho()
resetMatrix()
stroke(Colour.svg.SlateBlue)
strokeWidth(6)
noFill()
noSmooth()
rectMode(CORNERS)
pushMatrix()
translate(x-self.totallwidth-5,y)
scale(self.totallwidth + self.decwidth+10,r)
self.overlay:draw()
popMatrix()
rect(x-self.totallwidth-5,
y-r-5,
x+self.decwidth+5,
y+r+5)
popMatrix()
popStyle()
end
end
function NumberSpinner:activate(t)
t = t or {}
self.pos = t.pos or self.pos
self.maxdigits = t.maxdigits or self.maxdigits
self.maxdecs = t.maxdecs or self.maxdecs
if t.allowSignChange ~= nil then
self.allowsign = t.allowSignChange
else
self.allowsign = true
end
local value = t.value or 0
if value < 0 then
self.isneg = true
value = - value
end
local decs = value - math.floor(value)
value = math.floor(value)
local n = {}
local nn = 1
n[1] = math.floor(value%10)
value = math.floor(value/10)
while value > 0 do
table.insert(n,math.floor(value%10))
value = math.floor(value/10)
nn = nn + 1
end
self.numbers = n
self.numdigits = nn
self.maxdigits = math.max(self.maxdigits,nn)
n = {}
nn = 0
while decs > 0 and nn <= self.maxdecs do
decs = decs*10
table.insert(n,math.floor(decs))
decs = decs - math.floor(decs)
nn = nn + 1
end
self.decimals = n
self.numdecs = nn
--self.maxdecs = math.max(nn,self.maxdecs)
self.active = true
self.action = t.action or function() return true end
end
function NumberSpinner:deactivate()
self.numbers = {0}
self.numdigits = 1
self.decimals = {}
self.numdecs = 0
self.maxdecs = 5
self.maxdigits = 5
self.isneg = false
self.allowsign = true
self.pos = self.ipos
self.active = false
self.action = function() return true end
end
function NumberSpinner:getValue()
local value = 0
local ten = 1
for k,v in ipairs(self.numbers) do
value = value + (math.floor(v+.5)%10)*ten
ten = ten * 10
end
local ten = .1
for k,v in ipairs(self.decimals) do
value = value + (math.floor(v+.5)%10)*ten
ten = ten * .1
end
if self.isneg then
value = - value
end
return value
end
function NumberSpinner:isTouchedBy(touch)
if not self.active then
return false
end
local x,y = self.pos()
if touch.x < x - self.totalwidth then
self:deactivate()
return true
end
if touch.x > x + self.decwidth then
self:deactivate()
return true
end
if touch.y < y - self.radius then
self:deactivate()
return true
end
if touch.y > y + self.radius then
self:deactivate()
return true
end
self.intouch = true
return true
end
function NumberSpinner:processTouches(g)
if g.type.ended and g.type.tap and g.num == 2 then
local value = self:getValue()
if self.action and self.action(value) then
self:deactivate()
end
g:reset()
return
end
local x,y = self.pos()
local t = g.touchesArr[1]
if t.touch.state == BEGAN then
local n = math.floor((x - t.touch.x)/self.width + 1)
self.moving = n
else
if self.moving then
if self.moving > 0 then
if t.updated and self.numbers[self.moving] then
self.numbers[self.moving] = (self.numbers[self.moving] - t.touch.deltaY/HEIGHT*self.radius/10)%10
end
if not self.isneg
and self.moving == self.numdigits
and self.allowsign
and g.type.tap
and g.type.finished
then
self.isneg = true
end
if self.numdigits < self.maxdigits
and t.updated
and t.touch.x < x - self.numwidth - self.width then
table.insert(self.numbers,0)
self.numdigits = self.numdigits + 1
end
if t.updated
and self.moving == self.numdigits
and t.touch.x > x - self.numwidth + 2*self.width then
if self.numdigits > 1 then
table.remove(self.numbers)
self.numdigits = self.numdigits - 1
else
self.numbers[1] = 0
end
end
if self.moving == 1 and self.numdecs == 0 and self.maxdecs > 0 then
if t.updated
and t.touch.x > x + self.width then
table.insert(self.decimals,0)
self.numdecs = 1
end
end
if self.isneg
and self.moving > #self.numbers
and self.allowsign
and g.type.tap
and g.type.finished then
self.isneg = false
end
else
if t.updated and self.decimals[-self.moving] then
self.decimals[-self.moving] = (self.decimals[-self.moving] - t.touch.deltaY/HEIGHT*self.radius/10)%10
end
if self.numdecs < self.maxdecs
and t.touch.x > x + self.decwidth + 2*self.width then
table.insert(self.decimals,0)
self.numdecs = self.numdecs + 1
end
if t.updated
and self.moving == -self.numdecs
and t.touch.x < x + self.decwidth - 2*self.width then
table.remove(self.decimals)
self.numdecs = self.numdecs - 1
end
end
end
g:noted()
end
if (not g.type.tap and g.type.ended) or g.type.finished then
self.intouch = false
self.moving = nil
g:reset()
end
end
NumberSpinner.help = "Drag the wheels up and down to change the digits. Drag the outermost wheels to add or remove digits."
return NumberSpinner
--]==]
--[==[
-- Palm Rest
local PalmRest = class()
cimport "RoundedRectangle"
cimport "Coordinates"
function PalmRest:init(t)
t = t or {}
self.orientation = t.orientation
self.height = t.height or 0
local o = t.opacity or 127
self.mesh = mesh()
local w = math.max(WIDTH,HEIGHT)
self.width = w
self.mesh:addRect(w/2,-w/2-5,w+10,w+10)
self.mesh:setRectColor(1,0,0,0,o)
addRoundedRect({
mesh = self.mesh,
x = w/2,
y = 0,
width = 200,
height = 50,
corners = 9,
anchor = "south",
colour = color(0,0,0,o)
})
addRoundedRect({
mesh = self.mesh,
x = w/2,
y = 25,
width = 160,
height = 10,
radius = 3,
corners = 0,
anchor = "centre",
colour = color(127,127,127,o)
})
self.active = t.active or true
end
function PalmRest:draw()
if not self.active then
return
end
pushMatrix()
resetMatrix()
if self.orientation then
TransformOrientation(self.orientation)
end
translate((WIDTH - self.width)/2,self.height)
self.mesh:draw()
popMatrix()
end
function PalmRest:isTouchedBy(touch)
if not self.active then
return false
end
local v
if self.orientation then
v = OrientationInverse(self.orientation,vec2(touch.x,touch.y))
else
v = vec2(touch.x,touch.y)
end
if v.y < self.height then
return true
end
if v.y < self.height + 50 and math.abs(v.x - WIDTH/2) < 100 then
return true
end
return false
end
function PalmRest:processTouches(g)
for k,v in ipairs(g.touchesArr) do
if v.updated then
local x
if self.orientation then
x = OrientationInverse(
self.orientation,
vec2(v.touch.x,v.touch.y))
else
x = vec2(v.touch.x,v.touch.y)
end
if v.touch.state == BEGAN then
if x.y < self.height then
v:destroy()
end
else
local y
if self.orientation then
y = OrientationInverse(
self.orientation,
vec2(v.touch.prevX,v.touch.prevY))
else
y = vec2(v.touch.prevX,v.touch.prevY)
end
self.height = math.max(0,self.height + x.y - y.y)
end
end
end
if g.type.ended then
g:reset()
else
g:noted()
end
end
PalmRest.help = "The palm rest defines an area of the screen where touches are ignored. Drag the tab to set the height."
return PalmRest
--]==]
--[==[
-- PictureBrowser
local PictureBrowser = class()
cimport "RoundedRectangle"
cimport "Coordinates"
local Colour = unpack(cimport "Colour",nil)
cimport "ColourNames"
function PictureBrowser:init()
end
function PictureBrowser:setList(t)
t = t or {}
self.width = t.width or 100
self.height = t.height or 100
self.anchor = t.anchor or "centre"
self.camera = t.camera
local d = t.directory or "Documents"
local f = t.filter or function(n,w,h) return true end
local l = spriteList(d)
local ims = {}
local img,w
local c = 0
for k,v in ipairs(l) do
img = readImage(d .. ":" .. v)
if f(v,img.width,img.height) then
w = 100*math.min(1,img.width/img.height)
table.insert(ims,{img,w})
c = c + 1
end
end
if self.camera then
img = image(self.width,self.height)
setContext(img)
pushStyle()
pushMatrix()
resetStyle()
resetMatrix()
font("ArialRoundedMTBold")
fontSize(self.width/5)
fill(Colour.svg.LightGoldenrod)
RoundedRectangle(0,0,self.width,self.height,10)
fill(Colour.svg.Black)
text("Take",self.width/2,self.height/2 + self.width/10)
text("Photo",self.width/2,self.height/2 - self.width/10)
popStyle()
popMatrix()
setContext()
table.insert(ims,{img,100})
c = c + 1
end
self.images = ims
self.nimages = c
self.size = math.ceil(math.sqrt(c))
self.active = t.active or false
self.sep = t.sep or 10
self.fwidth = self.size*(self.width + self.sep) + self.sep
local n = math.ceil(self.nimages/self.size)
self.fheight = n*(self.width + self.sep) + self.sep
self.pos = t.pos or function()
return RectAnchorOf(Screen,"centre")
end
end
function PictureBrowser:draw()
if not self.active then
return
end
pushMatrix()
resetMatrix()
pushStyle()
resetStyle()
if self.usecamera then
spriteMode(CENTER)
local x,y = RectAnchorOf(Screen,"centre")
local w,h = spriteSize(CAMERA)
local ws = RectAnchorOf(Screen,"width") -10
local hs = RectAnchorOf(Screen,"height") -10
local asp = math.min(ws/w,hs/h,1)
sprite(CAMERA,x,y,w*asp,h*asp)
font("ArialRoundedMTBold")
fontSize(30)
fill(Colour.svg.LightGoldenrod)
ws = ws + 10
RoundedRectangle(0,0,ws,70,10)
fill(Colour.svg.Black)
text("Tap to take picture",ws/2,50)
text("Swipe to change camera",ws/2,20)
else
spriteMode(CORNER)
local x,y = self.pos()
x,y = RectAnchorAt(x,y,self.fwidth,self.fheight,self.anchor)
local w = self.width + self.sep
RoundedRectangle(x,y,self.fwidth,self.fheight,self.sep)
y = y + self.fheight - w
x = x + self.sep
local xx = x
for i=1,self.nimages do
sprite(self.images[i][1],x,y,self.images[i][2])
x = x + w
if x > xx + self.fwidth - w then
x = xx
y = y - w
end
end
end
popStyle()
popMatrix()
end
function PictureBrowser:isTouchedBy(touch)
if not self.active then
return false
end
if self.usecamera then
return true
end
local x,y = self.pos()
x,y = RectAnchorAt(x,y,self.fwidth,self.fheight,self.anchor)
if touch.x < x then
return false
end
if touch.y < y then
return false
end
if touch.x > x + self.fwidth then
return false
end
if touch.y > y + self.fheight then
return false
end
return true
end
function PictureBrowser:processTouches(g)
if g.type.ended then
if self.usecamera then
if g.type.moved then
if cameraSource() == CAMERA_FRONT then
cameraSource(CAMERA_BACK)
else
cameraSource(CAMERA_FRONT)
end
else
local img = image(CAMERA)
if self.callback(img) then
self:deactivate()
else
self.usecamera = false
end
end
else
local t = g.touchesArr[1]
local w = self.width + self.sep
local x,y = self.pos()
x,y = RectAnchorAt(x,y,self.fwidth,self.fheight,self.anchor)
y = y + self.fheight
x = math.ceil((t.touch.x - x)/w)
y = math.floor((y - t.touch.y)/w)
local n = self.size*y + x
if n > 0 and n <= self.nimages then
if self.camera and n == self.nimages then
self.usecamera = true
elseif self.callback(self.images[n][1]) then
self:deactivate()
end
end
end
g:reset()
else
g:noted()
end
end
function PictureBrowser:activate(f)
self.active = true
self.callback = f
self.usecamera = false
end
function PictureBrowser:deactivate()
self.active = false
self.callback = nil
self.usecamera = false
end
return PictureBrowser
--]==]
--[==[
-- Slider class
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
--[[
The "Slider" class draws a slider on the screen for the user to be
able to modify a parameter. When the slider is finished, a call-back
function is used to pass the new parameter value to the rest of the
program.
--]]
local Slider = class()
--[[
Our initial information consists of our end points (as vec2 objects)
and a colour.
--]]
function Slider:init(t)
t = t or {}
self.oa = t.a
self.ob = t.b
self:orientationChanged()
local d = self.b - self.a
d = vec2(-d.y,d.x)
self.d = d/d:len()
self.t = 0
self.fg = t.colour
self.bg = color(0,0,0,255)
end
function Slider:orientationChanged()
local x,y = self.oa()
self.a = vec2(x,y)
x,y = self.ob()
self.b = vec2(x,y)
end
--[[
This draws the slider and the bar at the current parameter value.
--]]
function Slider:draw()
if self.active then
local a = self.a
local b = self.b
local t = self.t
pushStyle()
strokeWidth(10)
stroke(self.bg)
line(a.x,a.y,b.x,b.y)
strokeWidth(6)
stroke(self.fg)
line(a.x,a.y,b.x,b.y)
local c = t*a + (1 - t)*b
local d = 20 * self.d
a = c - d
b = c + d
strokeWidth(10)
stroke(self.bg)
line(a.x,a.y,b.x,b.y)
strokeWidth(6)
stroke(self.fg)
line(a.x,a.y,b.x,b.y)
end
end
--[[
This is our activation function. Our arguments are: the current value
of the parameter, its minimum and maximum values, and two call-back
functions. The first is called when the slider is moved, the second
when it has finished moving. This means that the program can adjust
to changes in the value as they occur.
--]]
function Slider:activate(t,min,max,f,ff)
if type(t) == "table" then
min = t.min
max = t.max
f = t.action
ff = t.finalAction
t = t.value
end
self.active = true
t = (t - min)/(max - min)
self.t = math.min(math.max(0,t),1)
self.min = min
self.max = max
self.action = f
self.finalAction = ff
end
function Slider:deactivate()
self.active = false
self.action = nil
self.finalAction = nil
end
--[[
If we are active, we try to claim all touches.
--]]
function Slider:isTouchedBy(touch)
if self.active then
return true
end
end
--[[
We project the touch value onto the line and set the parameter value
accordingly, then call the appropriate call-back function.
We have to begin the touch near the slider, and there should be a way to cancel a slide ...
--]]
function Slider:processTouches(g)
if g.updated then
local t = g.touchesArr[1]
local a = self.a
local b = self.b
local c = a - b
local d = vec2(t.touch.x,t.touch.y)
d = d - b
local st = c:dot(d)
if t.touch.state == BEGAN then
local cl = c:len()
local ts = st/cl
if ts < -20 or ts > cl + 20 then
self:deactivate()
else
ts = ts/cl
local e = d - ts * c
if e:len() > 40 then
self:deactivate()
end
end
end
st = math.min(math.max(0,st/c:lenSqr()),1)
self.t = st
st = st*self.max + (1 - st)*self.min
if self.action then
self.action(st)
end
if g.type.ended then
self.active = false
if self.finalAction then
self.finalAction(st)
end
end
g:noted()
end
end
Slider.help = "The slider is used to change a continuous parameter between two given values. To change the parameter, drag the slider bar. To select a value, release the bar. Depending on how it was set up, you may see things change as you slide it, though it may be that not everything changes until the bar is released. You can cancel the slider at the start by touching some part of the screen well away from the slider."
return Slider
--]==]
--[==[
-- User interface class
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
--[[
This is a "User Interface" class. It is a container class that controls
the various pieces of the user interface: deciding whether they are drawn
and ensuring that they are registered with the "Touch Handler".
It is configured to use the "Touch Handler" class for touches, though
the interaction with that class is light so it could easily be
reconfigured to use some other system. It uses the "Font" class to
define a "System Font", though again this is more to ensure that the
elements in the user interface that need a font have one.
By default, the class initialises a keypad, a numeric pad, a "main
menu", two text areas (a small one for "System Messages" and a larger
one for "Information"), a colour picker, and a slider. Initially,
only the menu and the smaller text area are active (ie drawn): others
are activated on need.
The "User Interface" class provides some functions that are basically
wrappers around its elements. For example, there is a "getText(f)"
method which activates the keypad and allows the user to specify some
text string. The argument passed to this is a function which is
called when the user has finished specifying the string (such a
function is sometimes known as a "call-back"). This gets round the
problem that the program has to keep working while waiting for the
user to input the text.
Ideally, the "User Interface" is drawn last and is given first chance
at claiming a touch. This requires the user not to override these.
Also, temporary elements (such as the keypad) should be drawn on top
of the more permanent ones (such as the main menu) and the temporary
elements should be given priority for claiming touches. The class is
set up so that elements are drawn in the order in which they are
specified and touches are claimed in the reverse order.
--]]
local Font,_,Textarea = unpack(cimport "Font",nil)
cimport "Coordinates"
local Colour = unpack(cimport "Colour",nil)
local Menu,Keypad,ColourPicker,ColourWheel,Cubic,Keyboard,
Slider,NumberSpinner,PictureBrowser,FontPicker,PalmRest
local UI = class()
--[[
This is the initialiser function. The sole argument is a "Touch
Handler" (though it would make sense to allow varying the font and the
initial family of elements). We declare ourselves "active" (meaning
that the user interface elements are drawn) and initialise a few
standard elements: keypad, numeric pad, menu, message area,
information area, colour picker, and slider (not all are initially
active.
--]]
function UI:init(t)
self.touchHandler = t
self.nelts = 0
self.elements = {}
self.elementHandlers = t:unshiftHandlers()
self.systemfont = Font({name = "Courier", size = 16})
self.largefont = Font({name = "Courier", size = 32})
self.hugefont = Font({name = "Courier", size = 64})
self.keyboards = {}
self.timers = {}
self.active = true
self.screen = Screen
self.orientation = CurrentOrientation
self:supportedOrientations(ANY)
if cmodule.loaded "Menu" then
Menu = cimport "Menu"
self.mainMenu = Menu({
pos = function()
local x,y = RectAnchorOf(self.screen,"north west")
x = x + 10
y = y - 10
return x,y
end,
anchor = "north west",
font = self.systemfont})
self.mainMenu.dir = "x"
self.mainMenu.active = true
self.mainMenu.autoactive = false
-- cancel stuff might not be needed any longer
self.cancel = Menu({
pos = function()
local x,y = RectAnchorOf(self.screen,"north east")
x = x - 10
y = y - 10
return x,y
end,
anchor = "north east",
font = self.largefont
})
self.cancel.active = false
self.cancel:addItem({
title = "Cancel",
action = function()
self:cancelActivity()
end
})
self.tocancel = {}
self:addElement(
self.mainMenu,
self.cancel
)
end
if cmodule.loaded "Keypad" then
Keypad = cimport "Keypad"
self.keypad = Keypad({
pos = function()
local x,y = RectAnchorOf(self.screen,"west")
x = x + 50
return x,y
end,
anchor = "west",
width = 75,
height = 50,
colour = Colour.svg.Magenta
})
self.numpad = Keypad({
pos = function()
local x,y = RectAnchorOf(self.screen,"west")
x = x + 50
return x,y
end,
anchor = "west",
width = 75,
height = 50,
colour = Colour.svg.DeepPink,
numeric = true
})
self:addElement(
self.keypad,
self.numpad
)
self:addHelp({
title = "Key and numeric pads",
text = Keypad.help
})
end
if cmodule.loaded "Colour" then
_,ColourPicker,ColourWheel = unpack(cimport "Colour")
self.colourPicker = ColourPicker()
self:addElement(
self.colourPicker
)
self:addHelp({
title = "Colour Picker",
text = ColourPicker.help
})
self.colourWheel = ColourWheel()
self:addElement(
self.colourWheel
)
self:addHelp({
title = "Colour Wheel",
text = ColourWheel.help
})
self.useWheel = true
end
if cmodule.loaded "CubicSelector" then
Cubic = cimport "CubicSelector"
self.cubic = Cubic(vec4(0,1,0,0))
self:addElement(self.cubic)
self:addHelp({
title = "Cubic Curve",
text = Cubic.help
})
end
if cmodule.loaded "Slider" then
Slider = cimport "Slider"
self.slider = Slider({
a = function()
local x,y = RectAnchorOf(self.screen,"west")
x = x + 50
y = y +
math.min(200,2*RectAnchorOf(self.screen,"height")/3)
return x,y
end,
b = function()
local x,y = RectAnchorOf(self.screen,"west")
x = x + 50
y = y -
math.min(200,2*RectAnchorOf(self.screen,"height")/3)
return x,y
end,
colour = Colour.svg.Coral
})
self:addElement(
self.slider
)
self:addHelp({
title = "Slider",
text = Slider.help
})
end
if cmodule.loaded "NumberSpinner" then
NumberSpinner = cimport "NumberSpinner"
self.numberspinner = NumberSpinner({
-- font = self.hugefont
pos = function() return RectAnchorOf(self.screen,"centre") end
})
self:addElement(
self.numberspinner
)
self:addHelp({
title = "Number Spinner",
text = NumberSpinner.help
})
self.useSpinner = true
end
if cmodule.loaded "PictureBrowser" then
PictureBrowser = cimport "PictureBrowser"
self.picturebrowser = PictureBrowser()
self:addElement(self.picturebrowser)
end
if cmodule.loaded "FontPicker" then
FontPicker = cimport "FontPicker"
self.fontpicker = FontPicker()
self:addElement(self.fontpicker)
end
if cmodule.loaded "Font" then
_,_,Textarea = unpack(cimport "Font",nil)
self.messages = Textarea({
font = self.systemfont,
pos = function()
return 10,10
end,
width = "20em",
height = "4lh",
title = "System Messages"
})
-- self.messages.active = true
self.information = Textarea({
font = self.systemfont,
pos = function()
return RectAnchorOf(self.screen,"centre")
end,
width = "math.min(80em,WIDTH)",
height = "15lh",
anchor = "centre",
title = "Information",
vfill = true
})
self.notices = Textarea({
font = self.largefont,
pos = function()
local x,y = RectAnchorOf(self.screen,"centre")
return x,y
end,
width = "math.min(80em,WIDTH)",
height = "10lh",
anchor = "centre",
fit = true,
fadeTime = 2
})
self.helptxt = Textarea({
font = self.systemfont,
pos = function()
local x,y = RectAnchorOf(self.screen,"north east")
y = y - 50
return x,y
end,
width = "math.min(80em,WIDTH)",
height = "30lh",
anchor = "north east",
fit = true
})
self:addElement(
self.notices,
self.helptxt,
self.information,
self.messages
)
self:addHelp({
title = "Text boxes",
text = Textarea.help
})
end
if cmodule.loaded "PalmRest" then
PalmRest = cimport "PalmRest"
self.palmrest = PalmRest({
})
self:addElement(self.palmrest)
self:addHelp({
title = "Palm rest",
text = PalmRest.help
})
end
if cmodule.loaded "Keyboard" then
Keyboard = cimport "Keyboard"
end
end
--[[
This is the draw function. If we're active then we draw all our
elements (not all will actually draw: we just call their "draw"
method). We also ensure that if we're active then the main menu is
active (this is a sanity check as menus are sometimes deactivated by
their children). We ensure that the matrix and style are saved and
reset.
--]]
function UI:draw()
self:checkTimers()
if not self.active then
return false
end
pushMatrix()
pushStyle()
resetMatrix()
viewMatrix(matrix())
ortho()
TransformOrientation(self.orientation)
for k=self.nelts,1,-1 do
self.elements[k]:draw()
end
popMatrix()
popStyle()
end
--[[
This adds a new element, or new elements, to the user interface; for
example, a new menu. The arguments are a list of objects of
appropriate classes.
--]]
function UI:addElement(...)
for k,v in ipairs(arg) do
table.insert(self.elements,v)
table.insert(self.elementHandlers,
self.touchHandler:registerHandler(v))
self.nelts = self.nelts + 1
self:modifyTouchedBy(v)
self:modifyprocessTouches(v)
end
end
function UI:modifyTouchedBy(v)
local f = v.isTouchedBy
v.isTouchedBy = function(s,t)
t = TransformTouch(self.orientation,t)
return f(s,t)
end
end
function UI:modifyprocessTouches(v)
local f = v.processTouches
v.processTouches = function(s,g)
g:transformTouches(self.orientation)
return f(s,g)
end
end
function UI:activateElement(e)
local i,h
for k=self.nelts,1,-1 do
if self.elements[k] == e then
h = self.elementHandlers[k]
i = true
else
if i then
self.elements[k+1] = self.elements[k]
self.elementHandlers[k+1] = self.elementHandlers[k]
end
end
end
if i then
self.elements[1] = e
self.elementHandlers[1] = h
end
end
--[[
This adds a submenu to the main menu. The argument is the title
(which appears in the main menu). It returns the newly created menu
which can then have elements added to it (see the "Menu" class for
more on this).
--]]
function UI:addMenu(t)
t = t or {}
local x = t.x or 0
local y = t.y or 0
local pos = function() return x,y end
local ae = t.atEnd or false
local m
local mopts = {font = self.systemfont, pos = pos}
if not t.attach then
mopts.title = t.title
end
local tmopts = t.menuOpts or {}
for k,v in pairs(tmopts) do
mopts[k] = v
end
m = Menu(mopts)
if t.attach then
self.mainMenu:addItem({
title = t.title,
action = function(x,y)
m.x = x
m.y = y
if m.active then
m:deactivateDown()
else
m:activate()
end
end,
deselect = function()
m:deactivateDown()
end,
highlight = function()
return m.active
end,
atEnd = ae
})
end
self:addElement(m)
return m
end
--[[
The following all initiate some activity. We need a cancellation method.
--]]
function UI:cancelActivity()
for k,v in ipairs(self.tocancel) do
v:deactivate()
end
self.tocancel = {}
self.cancel:deactivate()
end
--[[
This is a wrapper around the keypad activation method. The argument
is a "call back" function that will be called when the keypad has
finished and the text entered by the user is available to the program
for use.
--]]
function UI:getText(f)
self:activateElement(self.keypad)
self.keypad:activate(f)
end
--[[
This is similar to the "getText" function except that it uses the
numeric keypad instead of the normal keypad.
--]]
function UI:getNumberPad(f)
self:activateElement(self.numpad)
self.numpad:activate(f)
end
--[[
This gets a number using a number spinner.
--]]
function UI:getNumberSpinner(...)
self:activateElement(self.numberspinner)
self.numberspinner:activate(...)
end
--[[
This gets a number using the preferred method.
--]]
function UI:getNumber(f)
if self.useSpinner then
self:getNumberSpinner({action = f})
else
self:getNumberPad(f)
end
end
--[[
This activates the colour picker. The two arguments are the list of
colours to use and the call-back function. See the "ColourPicker"
class for details of what these arguments should be like.
--]]
function UI:getColourPicker(t,f)
self:activateElement(self.colourPicker)
self.colourPicker:setList(t)
self.colourPicker:activate(f)
end
--[[
This activates the colour wheel. The argument is the call-back
function and initial/current colour.
--]]
function UI:getColourWheel(c,f)
self:activateElement(self.colourWheel)
self.colourWheel:activate(c,f)
end
--[[
This gets a colour using the preferred method.
--]]
function UI:getColour(t,f)
if type(t) == "string" then
self:getColourPicker(t,f)
return
end
if type(t) == "function" then
t,f = f,t
end
if self.useWheel then
if not t then
t = Colour.svg.Black
end
self:getColourWheel(t,f)
else
if type(t) ~= "string" then
t = ""
end
self:getColourPicker(t,f)
end
end
--[[
Gets a font name.
--]]
function UI:getFont(f)
self:activateElement(self.fontpicker)
self.fontpicker:activate({action = f})
end
--[[
Gets a picture as an image.
--]]
function UI:setPictureList(t)
self.picturebrowser:setList(t)
end
function UI:getPicture(f)
self:activateElement(self.picturebrowser)
self.picturebrowser:activate(f)
end
--[[
This is a wrapper for the cubic curve object.
--]]
function UI:getCurve(...)
self:activateElement(self.cubic)
self.cubic:activate(...)
end
--[[
This is a wrapper for the "slider" object, which allows the user to
specify a parameter by sliding a "slider". See the "Slider" class for
the allowed arguments to this function.
--]]
function UI:getParameter(...)
self:activateElement(self.slider)
self.slider:activate(...)
end
--[[
This adds a message to the "System Messages" text area, also ensuring
that this is active. See the "Textarea" class for the parameters to
pass to this.
--]]
function UI:addMessage(...)
if not ui.messages then
return false
end
ui.messages:addLine(...)
ui.messages:activate()
end
--[[
This is the same as "addMessage" except that it adds the message to
the "Information" text area.
--]]
function UI:addInformation(...)
if not ui.information then
return false
end
ui.information:addLine(...)
ui.information:activate()
end
--[[
This is the same as "addMessage" except that it adds the message to
the "Notices" text area and sets a timer.
--]]
function UI:addNotice(t)
if not ui.notices then
return false
end
local time = t.time or 7
if type(t.text) == "table" then
ui.notices:setLines(unpack(t.text))
else
ui.notices:setLines(t.text)
end
ui.notices.fade = t.fadeTime or 2
ui.notices:activate()
ui:setTimer(time,function() ui.notices:deactivate() return true end)
end
function UI:setTimer(t,f)
table.insert(self.timers,{t + ElapsedTime,f})
end
function UI:checkTimers()
local rm,n = {},0
for k,v in ipairs(self.timers) do
if ElapsedTime > v[1] then
if v[2]() then
table.insert(rm,k)
n = n + 1
end
end
end
if n > 0 then
for k = n,1,-1 do
table.remove(self.timers,rm[k])
end
end
end
function UI:reset()
for k,v in ipairs(self.elements) do
if v.reset and type(v.reset) == "function" then
v:reset()
end
end
end
function UI:pause(p)
for k,v in ipairs(self.elements) do
if v.pause and type(v.pause) == "function" then
v:pause(p)
end
end
end
function UI:orientationChanged(o)
if not self.allowedOrientations[o] then
return
end
self.orientation = o
self.screen = Screen
for k,v in ipairs(self.elements) do
if v.orientationChanged and type(v.orientationChanged) == "function" then
v:orientationChanged(o)
end
end
end
function UI:setOrientation(o,t)
if t then
self.allowedOrientations = t
end
o = ResolveOrientation(o,CurrentOrientation)
self.orientation = o
if o == PORTRAIT or o == PORTRAIT_UPSIDE_DOWN then
self.screen = Portrait
else
self.screen = Landscape
end
for k,v in ipairs(self.elements) do
if v.orientationChanged and type(v.orientationChanged) == "function" then
v:orientationChanged(o)
end
end
end
function UI:supportedOrientations(...)
self.allowedOrientations = {}
for _,v in ipairs(arg) do
if v < 4 then
self.allowedOrientations[v] = true
elseif v == 4 then
self.allowedOrientations[0] = true
self.allowedOrientations[1] = true
elseif v == 5 then
self.allowedOrientations[2] = true
self.allowedOrientations[3] = true
elseif v == 6 then
self.allowedOrientations[0] = true
self.allowedOrientations[1] = true
self.allowedOrientations[2] = true
self.allowedOrientations[3] = true
end
end
end
function UI:declareKeyboard(...)
local kbd = Keyboard(...)
ui:addElement(kbd)
self.keyboards[kbd.type] = kbd
return kbd
end
function UI:useKeyboard(t,f)
if self.keyboards[t] then
self:activateElement(self.keyboards[t])
self.keyboards[t]:activate(f)
self.activekbd = self.keyboards[t]
end
end
function UI:unuseKeyboard(t)
if t then
if self.keyboards[t] then
self.keyboards[t]:deactivate()
if self.activekbd == self.keyboards[t] then
self.activekbd = nil
end
end
else
if self.activekbd then
self.activekbd:deactivate()
self.activekbd = nil
end
end
end
function UI:keyboardtop()
if self.activekbd then
return self.activekbd.top
else
return 0
end
end
function UI:systemmenu()
local m = self:addMenu({title = "Main", attach = true})
if fullscreen then
m:addItem({
title = "Fullscreen",
action = function()
fullscreen()
return true
end,
highlight = function()
return displayMode() ~= STANDARD
end
})
end
if pause then
m:addItem({
title = "Pause",
action = function()
pause()
return true
end,
highlight = function()
return paused
end
})
end
if reset then
m:addItem({
title = "Reset",
action = function()
reset()
return true
end
})
end
m:addItem({
title = "Start Recording",
action = function() DoAtEndDraw(function()
if not isRecording() then
startRecording()
end
return true
end)
return true
end
})
m:addItem({
title = "Stop Recording",
action = function() DoAtEndDraw(function()
if isRecording() then
stopRecording()
end
return true
end)
return true
end
})
if hide then
m:addItem({
title = "Hide",
action = function()
hide()
return true
end
})
end
m:addItem({
title = "Reset touches",
action = function()
self.touchHandler:reset()
return true
end})
m:addItem({
title = "Exit",
action = function()
close()
return true
end})
paused = false
return m
end
function UI:helpmenu()
local m = self:addMenu({title = "Help", attach = true, atEnd = true})
self.helpm = m
if self.helps then
for k,v in ipairs(self.helps) do
m:addItem({
title = v[1],
action = function()
if self.helptxt.active then
self.helptxt:deactivate()
else
self.helptxt:activate()
self.helptxt:setLines(unpack(v[2]))
end
return true
end
})
end
end
return m
end
function UI:addHelp(t)
t = t or {}
local text = t.text
if type(text) ~= "table" then
text = {text}
end
table.insert(text,"(Double-tap this box to hide it.)")
if self.helpm then
self.helpm:addItem({
title = t.title,
action = function()
if self.helptxt.active then
self.helptxt:deactivate()
else
self.helptxt:activate()
self.helptxt:setLines(unpack(text))
end
return true
end
})
else
self.helps = self.helps or {}
table.insert(self.helps,{t.title,text})
end
end
function UI:removeHelp(t)
if self.helpm then
return self.helpm:removeItem(t)
elseif self.helps then
for k,v in ipairs(self.helps) do
if v.title == t then
table.remove(self.helps,k)
return v
end
end
end
end
function UI:hide(t)
self:addNotice({text = "The hidden things will return after " .. t .. " seconds"})
self:setTimer(7,function() self.active = false return true end)
self:setTimer(t+7,function() unhide() return true end)
end
function UI:unhide()
self.active = true
end
--[[
Helper functions to delay stuff to the end of the current draw function. To
make use of these, you must call AtEndOfDraw() in the draw function (at the
end, obviously).
--]]
function AtEndOfDraw()
local t = atenddraw or {}
local s = {}
for k,v in ipairs(t) do
if not v() then
table.insert(s,v)
end
end
atenddraw = s
end
function DoAtEndDraw(f)
atenddraw = atenddraw or {}
table.insert(atenddraw,f)
end
cmodule.gexport {
AtEndOfDraw = AtEndOfDraw,
DoAtEndDraw = DoAtEndDraw
}
return UI
--]==]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment