1212 lines
32 KiB
Lua
1212 lines
32 KiB
Lua
--[[
|
|
This file is part of Ice Lua Components.
|
|
|
|
Ice Lua Components is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Lesser General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Ice Lua Components is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public License
|
|
along with Ice Lua Components. If not, see <http://www.gnu.org/licenses/>.
|
|
]]
|
|
|
|
if client then
|
|
|
|
-- img_blit_to segfaults prior to 0.2.1
|
|
GUI_USE_VA_PRINT = (common.version.num >= 8421376+10)
|
|
|
|
dofile("pkg/base/lib_util.lua")
|
|
|
|
-- load images
|
|
img_font_numbers = skin_load("png", "font-numbers.png", DIR_PKG_GFX)
|
|
img_font_mini = skin_load("tga", "font-mini.tga", DIR_PKG_GFX)
|
|
img_font_large = skin_load("tga", "font-large.tga", DIR_PKG_GFX)
|
|
img_tiles_roundrect = skin_load("tga", "roundrect.tga", DIR_PKG_GFX)
|
|
|
|
--[[
|
|
client.img_free(img_font_numbers)
|
|
img_font_numbers = nil -- PLEASE DO THIS, GUYS!
|
|
]]
|
|
|
|
local digit_map = {
|
|
[string.byte(" ")] = 0,
|
|
[string.byte("0")] = 1,
|
|
[string.byte("1")] = 2,
|
|
[string.byte("2")] = 3,
|
|
[string.byte("3")] = 4,
|
|
[string.byte("4")] = 5,
|
|
[string.byte("5")] = 6,
|
|
[string.byte("6")] = 7,
|
|
[string.byte("7")] = 8,
|
|
[string.byte("8")] = 9,
|
|
[string.byte("9")] = 10,
|
|
[string.byte("-")] = 11,
|
|
}
|
|
|
|
-- TODO: find a better solution than this shit
|
|
-- y'know, just in case someone decides they're going to play this with an AZERTY
|
|
local shift_map = {
|
|
["1"] = "!", ["2"] = "@", ["3"] = "#", ["4"] = "$", ["5"] = "%",
|
|
["6"] = "^", ["7"] = "&", ["8"] = "*", ["9"] = "(", ["0"] = ")",
|
|
["`"] = "~", ["-"] = "_", ["="] = "+",
|
|
["["] = "{", ["]"] = "}", ["\\"] = "|",
|
|
[";"] = ":", ["'"] = "\"",
|
|
[","] = "<", ["."] = ">", ["/"] = "?",
|
|
}
|
|
|
|
local DIGIT_WIDTH = 32
|
|
local DIGIT_HEIGHT = 48
|
|
local MINI_WIDTH = 6
|
|
local MINI_HEIGHT = 8
|
|
local LARGE_WIDTH = 24
|
|
local LARGE_HEIGHT = 32
|
|
|
|
function gui_index_mini(idx) return idx-32 end
|
|
function gui_index_digit(idx) return digit_map[idx] end
|
|
|
|
-- create a new fixed-width font using the bitmap image, character width and height, and char indexing function
|
|
function gui_create_fixwidth_font(image, char_width, char_height, indexing_fn, shadow)
|
|
if GUI_USE_VA_PRINT then
|
|
local src_image = image
|
|
local w, h = client.img_get_dims(src_image)
|
|
local i
|
|
i = 64 while i < w do i = i * 2 end w = i
|
|
i = 64 while i < h do i = i * 2 end h = i
|
|
image = client.img_new(w, h)
|
|
-- segfault happens
|
|
client.img_blit_to(image, src_image, 0, 0)
|
|
end
|
|
|
|
local this = {image=image, width=char_width, height=char_height,
|
|
indexing_fn=indexing_fn, shadow=shadow}
|
|
|
|
-- compute a non-wrapped characters + positions output suitable for usage in text selections as well as render
|
|
function this.compute_unwrapped(x, y, c, str)
|
|
|
|
result = {{}}
|
|
local col_count = 1
|
|
|
|
for col_count=1, #str do
|
|
local char = string.byte(str, col_count)
|
|
result[1][col_count] = {char, this.indexing_fn(char), x, y, c}
|
|
x = x + this.width
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
-- compute characters + positions from a colortable, where each line of table contains a string "msg" and a color "color"
|
|
function this.compute_ctab(ctab, x, y)
|
|
local result = {}
|
|
|
|
for k, v in pairs(ctab) do
|
|
-- compute and append a single line
|
|
local line = this.compute_unwrapped(x, y, v.color, v.msg)
|
|
local i
|
|
for i=0, #line do
|
|
table.insert(result, line[i])
|
|
end
|
|
-- then add the y of the last character + a newline
|
|
y = line[#line][#line[#line]][4] + this.height
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- compute a wordwrapped characters + positions output suitable for usage in text selections as well as render
|
|
function this.compute_wordwrap(wp, x, y, c, str)
|
|
|
|
-- 1. find whitespace
|
|
|
|
if wp < this.width then wp = this.width end -- force width to at least 1 char
|
|
|
|
local i
|
|
local j
|
|
local toks = {}
|
|
local cur_tok = 1
|
|
local line_charwidth = math.floor(wp / this.width)
|
|
|
|
for i=1,#str do
|
|
local idx = string.byte(str, i)
|
|
if idx == 13 or idx == 10 then -- CR/LF
|
|
if toks[cur_tok] == nil then
|
|
toks[cur_tok] = {newlines=1}
|
|
elseif toks[cur_tok].newlines == nil then
|
|
cur_tok = cur_tok + 1; toks[cur_tok] = {newlines=1}
|
|
else
|
|
toks[cur_tok].newlines = toks[cur_tok].newlines + 1
|
|
end
|
|
elseif idx == 32 then -- space
|
|
if toks[cur_tok] == nil then
|
|
toks[cur_tok] = {spaces=1}
|
|
elseif toks[cur_tok].spaces == nil then
|
|
cur_tok = cur_tok + 1; toks[cur_tok] = {spaces=1}
|
|
else
|
|
toks[cur_tok].spaces = toks[cur_tok].spaces + 1
|
|
end
|
|
else -- word
|
|
if toks[cur_tok] == nil then
|
|
toks[cur_tok] = {word={idx}}
|
|
elseif toks[cur_tok].word == nil or
|
|
#toks[cur_tok].word+1 > line_charwidth -- split if word is larger than a line
|
|
then
|
|
cur_tok = cur_tok + 1; toks[cur_tok] = {word={idx}}
|
|
else
|
|
toks[cur_tok].word[#toks[cur_tok].word+1] = idx
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 2. render as many words as possible per line to a cacheable character buffer
|
|
|
|
local begin_x = x
|
|
local end_x = x + wp
|
|
local result = {{}}
|
|
local line_count = 1
|
|
local col_count = 1
|
|
|
|
local function endline()
|
|
x = begin_x; y = y + this.height
|
|
line_count = line_count + 1
|
|
result[line_count] = {}
|
|
col_count = 1
|
|
end
|
|
|
|
for i=1,#toks do
|
|
local tok = toks[i]
|
|
if tok.word ~= nil then
|
|
if x + #tok.word * this.width > end_x then endline() end
|
|
for j=1,#tok.word do
|
|
result[line_count][col_count] = {tok.word[j],
|
|
this.indexing_fn(tok.word[j]), x, y, c}
|
|
x = x + this.width
|
|
col_count = col_count + 1
|
|
end
|
|
elseif tok.spaces ~= nil then
|
|
local char = string.byte(' ')
|
|
result[line_count][col_count] = {char,
|
|
this.indexing_fn(char), x, y, c}
|
|
x = x + this.width * tok.spaces
|
|
col_count = col_count + 1
|
|
elseif tok.newlines ~= nil then
|
|
for j=1,tok.newlines do endline() end
|
|
end
|
|
if x > end_x then endline() end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
-- get the AABB dimensions of text given precomputed text data
|
|
function this.dimensions(data)
|
|
|
|
if #data<1 or #data[1]<1 then return {l=0,r=0,t=0,b=0,width=1,height=1} end
|
|
|
|
local result = {l=data[1][1][3],
|
|
r=data[1][1][3] + this.width,
|
|
t=data[1][1][4],
|
|
b=data[1][1][4] + this.height,
|
|
width=0, height=0}
|
|
|
|
local row = 1
|
|
local col = 1
|
|
|
|
for row=1,#data do
|
|
for col=1,#data[row] do
|
|
result.l = math.min(result.l, data[row][col][3])
|
|
result.r = math.max(result.r, data[row][col][3] + this.width)
|
|
result.t = math.min(result.t, data[row][col][4])
|
|
result.b = math.max(result.b, data[row][col][4] + this.height)
|
|
end
|
|
end
|
|
|
|
result.width = result.r - result.l
|
|
result.height = result.b - result.t
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
function this._blit(buffer, x, y, idx, c)
|
|
if this.shadow then
|
|
if buffer == nil then
|
|
client.img_blit(this.image, x+1, y+1, this.width, this.height, idx*this.width, 0, this.shadow)
|
|
else
|
|
client.img_blit_to(buffer, this.image, x+1, y+1, this.width, this.height, idx*this.width, 0, this.shadow)
|
|
end
|
|
end
|
|
if buffer == nil then
|
|
client.img_blit(this.image, x, y, this.width, this.height, idx*this.width, 0, c)
|
|
else
|
|
client.img_blit_to(buffer, this.image, x, y, this.width, this.height, idx*this.width, 0, c)
|
|
end
|
|
end
|
|
|
|
-- calculate the shadow strength from the percieved luminance of the font; brighter color = darker shadow
|
|
function this.calc_shadow(c)
|
|
local a, r, g, b = argb_merged_to_split(c)
|
|
local luminance = (0.2126 * r/256 + 0.7152 * g/256 + 0.0722 * b/256) -- Photometric/digital ITU-R
|
|
this.shadow = argb_split_to_merged(0,0,0,(luminance)*256)
|
|
end
|
|
|
|
-- print text with topleft at x, y, color c, string str
|
|
function this.print(x, y, c, str, buffer)
|
|
this.calc_shadow(c)
|
|
if GUI_USE_VA_PRINT and buffer == nil then
|
|
local i
|
|
local iw, ih = client.img_get_dims(this.image)
|
|
local sw, sh = client.screen_get_dims()
|
|
local xscale = 2/sw
|
|
local yscale = 2/sw
|
|
local xadv = xscale
|
|
local yadv = yscale
|
|
local wmul = 1/iw
|
|
local hmul = 1/ih
|
|
local v0 = this.height/ih
|
|
local v1 = 0.0
|
|
|
|
x = (x-sw/2)*xscale
|
|
y = (y-sh/2)*yscale
|
|
local y0 = y + yscale*this.height
|
|
local y1 = y
|
|
local z0 = 1.0
|
|
local a0,r0,g0,b0 = common.argb_merged_to_split(c)
|
|
local a1,r1,g1,b1 = common.argb_merged_to_split(this.shadow)
|
|
r0 = r0/255
|
|
g0 = g0/255
|
|
b0 = b0/255
|
|
a0 = a0/255
|
|
r1 = r1/255
|
|
g1 = g1/255
|
|
b1 = b1/255
|
|
a1 = a1/255
|
|
local l = {}
|
|
local la = {}
|
|
for i=1,#str do
|
|
local idx = this.indexing_fn(string.byte(str, i))
|
|
idx = idx*this.width
|
|
local u0 = (idx+0)*wmul
|
|
local u1 = (idx+this.width)*wmul
|
|
|
|
local x0 = x
|
|
local x1 = x + xscale*this.width
|
|
x = x1
|
|
|
|
la[1+#la] = {-x0,y0,z0,r0,g0,b0,a0,u0,v0}
|
|
la[1+#la] = {-x1,y0,z0,r0,g0,b0,a0,u1,v0}
|
|
la[1+#la] = {-x0,y1,z0,r0,g0,b0,a0,u0,v1}
|
|
la[1+#la] = {-x0,y1,z0,r0,g0,b0,a0,u0,v1}
|
|
la[1+#la] = {-x1,y0,z0,r0,g0,b0,a0,u1,v0}
|
|
la[1+#la] = {-x1,y1,z0,r0,g0,b0,a0,u1,v1}
|
|
x0 = x0 + xadv
|
|
x1 = x1 + xadv
|
|
y0 = y0 + yadv
|
|
y1 = y1 + yadv
|
|
l[1+#l] = {-x0,y0,z0,r1,g1,b1,a1,u0,v0}
|
|
l[1+#l] = {-x1,y0,z0,r1,g1,b1,a1,u1,v0}
|
|
l[1+#l] = {-x0,y1,z0,r1,g1,b1,a1,u0,v1}
|
|
l[1+#l] = {-x0,y1,z0,r1,g1,b1,a1,u0,v1}
|
|
l[1+#l] = {-x1,y0,z0,r1,g1,b1,a1,u1,v0}
|
|
l[1+#l] = {-x1,y1,z0,r1,g1,b1,a1,u1,v1}
|
|
y0 = y0 - yadv
|
|
y1 = y1 - yadv
|
|
|
|
end
|
|
|
|
local i
|
|
for i=1,#la do
|
|
l[1+#l] = la[i]
|
|
end
|
|
|
|
this.va = common.va_make(l, this.va, "3v,4c,2t")
|
|
client.gfx_depth_mask(false)
|
|
if USE_GLSL_20 and shader_img then
|
|
shader_img.push()
|
|
client.va_render_local(this.va, 0, 0, 0, 0, 0, 0, 1, this.image, "ah")
|
|
shader_img.pop()
|
|
else
|
|
if USE_GLSL_20 and shader_push_nil then shader_push_nil() end
|
|
client.va_render_local(this.va, 0, 0, 0, 0, 0, 0, 1, this.image, "ah")
|
|
if USE_GLSL_20 and shader_push_nil then shader_pop_nil() end
|
|
end
|
|
client.gfx_depth_mask(true)
|
|
else
|
|
local i
|
|
for i=1,#str do
|
|
local idx = this.indexing_fn(string.byte(str, i))
|
|
this._blit(buffer, x, y, idx, c)
|
|
x = x + this.width
|
|
end
|
|
end
|
|
end
|
|
|
|
-- print a selection of precomputed text
|
|
function this.print_precomputed(data, offx, offy, buffer)
|
|
|
|
local lastc = nil
|
|
|
|
for y=1,#data do
|
|
for x=1,#data[y] do
|
|
local char = data[y][x][1]
|
|
local idx = data[y][x][2]
|
|
local px = data[y][x][3] + offx
|
|
local py = data[y][x][4] + offy
|
|
local c = data[y][x][5]
|
|
if c ~= lastc then lastc = c; this.calc_shadow(c) end
|
|
this._blit(buffer, px, py, idx, c)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- print text with minimum-space wordwrapping, pixelwidth wp, topleft at x, y, color c, string str
|
|
function this.print_wrap(wp, x, y, c, str, buffer)
|
|
this.print_precomputed(this.compute_wordwrap(wp, x, y, c, str), 0, 0, buffer)
|
|
end
|
|
|
|
return this
|
|
end
|
|
|
|
font_mini = gui_create_fixwidth_font(img_font_mini, MINI_WIDTH, MINI_HEIGHT, gui_index_mini, true)
|
|
font_large = gui_create_fixwidth_font(img_font_large, LARGE_WIDTH, LARGE_HEIGHT, gui_index_mini, true)
|
|
font_digits = gui_create_fixwidth_font(img_font_numbers, DIGIT_WIDTH, DIGIT_HEIGHT, gui_index_digit)
|
|
|
|
function gui_get_char(key, modif)
|
|
if key >= 32 and key <= 126 then
|
|
local shifted = (bit_and(modif, KMOD_SHIFT) ~= 0)
|
|
local crapslock = (bit_and(modif, KMOD_CAPS) ~= 0)
|
|
if key >= SDLK_a and key <= SDLK_z then
|
|
if shifted ~= crapslock then
|
|
key = key - 32
|
|
end
|
|
end
|
|
|
|
local k = string.char(key)
|
|
k = (shifted and shift_map[k]) or k
|
|
return k
|
|
end
|
|
|
|
-- TODO: check some other things
|
|
|
|
return nil
|
|
end
|
|
|
|
function gui_string_edit(str, insert_position, maxlen, key, modif, uni)
|
|
if key == SDLK_BACKSPACE then
|
|
if insert_position <= 1 then
|
|
str = string.sub(str, 2, #str)
|
|
else
|
|
str = string.sub(str, 1, insert_position - 2) ..
|
|
string.sub(str, insert_position, #str)
|
|
end
|
|
elseif key == SDLK_DELETE then
|
|
if insert_position <= 1 then
|
|
str = string.sub(str, 2, #str)
|
|
else
|
|
str = string.sub(str, 1, insert_position - 1) ..
|
|
string.sub(str, insert_position + 1, #str)
|
|
end
|
|
else
|
|
--local k = gui_get_char(key, modif)
|
|
local k = uni
|
|
if (not k) or (k < 32 or k >= 127) then
|
|
k = nil
|
|
else
|
|
k = string.char(k)
|
|
end
|
|
|
|
if #str < maxlen and k then
|
|
str = string.sub(str, 1, insert_position - 1) ..
|
|
k ..
|
|
string.sub(str, insert_position, #str)
|
|
end
|
|
end
|
|
|
|
return str
|
|
end
|
|
|
|
function gui_string_delete(str, position)
|
|
if position <= 1 then
|
|
str = string.sub(str, 2, #str)
|
|
else
|
|
str = string.sub(str, 1, position - 2) ..
|
|
string.sub(str, position, #str)
|
|
end
|
|
|
|
return str
|
|
end
|
|
|
|
function gui_string_append(str, other, position)
|
|
if position == 1 then
|
|
return other .. str
|
|
elseif position == #str then
|
|
return str .. other
|
|
else
|
|
str_len = #str
|
|
str = string.sub(str, 1, position - 1) ..
|
|
other ..
|
|
string.sub(str, position, str_len)
|
|
return str
|
|
end
|
|
end
|
|
|
|
--[[Create a new scene.
|
|
Each scene contains its own displaylist, buffers, and listeners.]]
|
|
function gui_create_scene(width, height, shared_rate)
|
|
|
|
local scene = {}
|
|
|
|
function scene.display_object(options)
|
|
local this = widgets.widget(options)
|
|
|
|
this.visible = options.visible
|
|
if this.visible == nil then this.visible = true end -- draws this node and children
|
|
this.drawable = options.drawable
|
|
if this.drawable == nil then this.drawable = true end -- calls the draw method
|
|
options.dirty = true -- whether drawing needs to be updated
|
|
this.use_img = options.use_img or false -- allocates a img buffer to this node
|
|
this.img = options.img or nil
|
|
this.listeners = options.listeners or {}
|
|
this.alarms = options.alarms or {} -- ticked if seen. Will NOT dispose finished alarms for you!
|
|
this.static_alarms = options.static_alarms or {}
|
|
|
|
function this.free()
|
|
common.img_free(this.img) for k,v in pairs(this.children) do v.free() end
|
|
end
|
|
-- stub for actual drawing
|
|
function this.draw_update() end
|
|
-- draw this and the child, if possible.
|
|
function this.draw()
|
|
if this.visible then
|
|
if this.drawable then
|
|
if this.use_img then -- draw using img buffer
|
|
this.detect_bufsize_change()
|
|
if this.dirty then
|
|
this.draw_update()
|
|
this.dirty = false
|
|
end
|
|
client.img_blit(this.img, this.relx, this.rely)
|
|
else -- draw using some other method?
|
|
this.draw_update()
|
|
end
|
|
end
|
|
for k,v in pairs(this.children) do v.draw() end
|
|
end
|
|
end
|
|
function this.detect_bufsize_change()
|
|
local cw, ch = math.ceil(this.width), math.ceil(this.height)
|
|
if this.img == nil then
|
|
this.img = common.img_new(cw, ch)
|
|
else
|
|
local pw, ph
|
|
pw, ph = common.img_get_dims(this.img)
|
|
pw, ph = math.ceil(pw), math.ceil(ph)
|
|
if not (pw == cw and ph == ch) then
|
|
common.img_free(this.img)
|
|
this.img = common.img_new(cw, ch)
|
|
end
|
|
end
|
|
end
|
|
function this.add_listener(ge_type, callback)
|
|
if ge_type == nil then error("nil ge_type") end
|
|
if callback == nil then error("nil callback") end
|
|
local l = this.listeners
|
|
if not l[ge_type] then l[ge_type] = {} end
|
|
table.insert(l[ge_type], callback)
|
|
end
|
|
-- given a dT and list of events [ge_type, data] call the listeners with matching type and progress any alarms
|
|
function this.pump_listeners(dT, events)
|
|
local flat = this.flatten()
|
|
for k, v in pairs(flat) do
|
|
if v.alarms ~= nil then
|
|
for i, j in pairs(v.alarms) do
|
|
j.tick(dT)
|
|
end
|
|
end
|
|
if v.static_alarms ~= nil then
|
|
for i, j in pairs(v.static_alarms) do
|
|
j.tick(dT)
|
|
end
|
|
end
|
|
for i=1, #events do
|
|
local ev = events[i]
|
|
if v.listeners ~= nil then
|
|
local listen_set = v.listeners[ev[1]]
|
|
if listen_set ~= nil then
|
|
for j=1, #listen_set do
|
|
listen_set[j](ev[2])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
--Declare a self-cleaning alarm using the common.lua alarm syntax.
|
|
function this.alarm(options)
|
|
local a = alarm(options)
|
|
table.insert(this.alarms, a)
|
|
local on_trigger = a.on_trigger
|
|
local function wrap()
|
|
on_trigger()
|
|
for i=1, #this.alarms do
|
|
if this.alarms[i] == a then table.remove(this.alarms, i) break end
|
|
end
|
|
end
|
|
a.on_trigger = wrap
|
|
end
|
|
--[[Declare a self-cleaning alarm using the common.lua alarm syntax, plus a
|
|
"name" option so that at most 1 of this alarm exists at one time.]]
|
|
function this.static_alarm(options)
|
|
local a = alarm(options)
|
|
if options.name == nil then error('no static alarm name given') end
|
|
this.static_alarms[options.name] = a
|
|
local on_trigger = a.on_trigger
|
|
local function wrap()
|
|
on_trigger()
|
|
this.static_alarms[options.name] = nil
|
|
end
|
|
a.on_trigger = wrap
|
|
end
|
|
return this
|
|
end
|
|
|
|
local root = scene.display_object{x=0, y=0,
|
|
width=width, height=height, align_x=0, align_y=0}
|
|
scene.root = root
|
|
|
|
function scene.hspacer(options)
|
|
local this = widgets.hspacer(options)
|
|
if options.visible~=nil then this.visible = options.visible else this.visible = true end
|
|
this.draw = function()
|
|
if this.visible then
|
|
for k,v in pairs(this.children) do v.draw() end
|
|
end
|
|
end
|
|
return this
|
|
end
|
|
|
|
function scene.vspacer(options)
|
|
local this = widgets.vspacer(options)
|
|
if options.visible~=nil then this.visible = options.visible else this.visible = true end
|
|
this.draw = function()
|
|
if this.visible then
|
|
for k,v in pairs(this.children) do v.draw() end
|
|
end
|
|
end
|
|
return this
|
|
end
|
|
|
|
local shared_rate = shared_rate or 1./60
|
|
local sharecount = 0
|
|
|
|
function scene.pump_listeners(dT, events)
|
|
-- copy incoming events
|
|
local e = {}
|
|
local i
|
|
for i=1, #events do
|
|
table.insert(e, events[i])
|
|
end
|
|
-- tick timers
|
|
sharecount = sharecount + dT
|
|
while sharecount > shared_rate do
|
|
table.insert(e, {GE_SHARED_ALARM, sharecount})
|
|
sharecount = sharecount - shared_rate
|
|
end
|
|
table.insert(e, {GE_DELTA_TIME, dT})
|
|
-- propogate
|
|
root.pump_listeners(dT, e)
|
|
end
|
|
|
|
function scene.draw() root.draw() end
|
|
function scene.free() root.free() end
|
|
|
|
function scene.rect_frame(options)
|
|
|
|
local this = scene.display_object(options)
|
|
|
|
this.frame_col = options.frame_col or 0xFF888888
|
|
this.fill_col = options.fill_col or 0xFFAAAAAA
|
|
|
|
this.use_img = true
|
|
this.dirty = true
|
|
|
|
function this.draw_update()
|
|
local w = math.ceil(this.width)
|
|
local h = math.ceil(this.height)
|
|
local img = this.img
|
|
local frame_col = this.frame_col
|
|
local fill_col = this.fill_col
|
|
common.img_fill(img, fill_col)
|
|
for ix = 0, w-1, 1 do
|
|
common.img_pixel_set(img, ix, 0, frame_col)
|
|
common.img_pixel_set(img, ix, h-1, frame_col)
|
|
end
|
|
for iy = 0, h-1, 1 do
|
|
common.img_pixel_set(img, 0, iy, frame_col)
|
|
common.img_pixel_set(img, w-1, iy, frame_col)
|
|
end
|
|
end
|
|
|
|
return this
|
|
|
|
end
|
|
|
|
--[[
|
|
Draws any number of sampled waveforms as a graph.
|
|
To use, "sample_sets" should contain at least one table of
|
|
{{samples},color_1,color_2,low_lim,hi_lim} where
|
|
samples is any number of floating point values,
|
|
color_1 is the "light" color of the waveform(body/highlights),
|
|
color_2 is the "dark" color of the waveform(edges),
|
|
low_lim is the lower limit of the samples,
|
|
hi_lim is higher limit of the samples.
|
|
]]
|
|
function scene.waveform(options)
|
|
|
|
local this = scene.display_object(options)
|
|
|
|
this.sample_sets = options.sample_sets or {
|
|
{0.},0xFF888888,0xFF444444,-1,1}
|
|
this.bg_col = options.bg_col or 0x44000000
|
|
this.midpoint_col = options.bg_col or 0x44888888
|
|
|
|
this.use_img = true
|
|
this.dirty = true
|
|
|
|
function this.draw_update()
|
|
local w = math.ceil(this.width)
|
|
local h = math.ceil(this.height)
|
|
local img = this.img
|
|
local bg_col = this.bg_col
|
|
local midpoint_col = this.midpoint_col
|
|
common.img_fill(img, bg_col)
|
|
|
|
local half_h = h/2
|
|
local lim = h / 2 - 1;
|
|
|
|
for k,sample_packet in pairs(this.sample_sets) do
|
|
local samples = sample_packet[1]
|
|
local col_1 = sample_packet[2]
|
|
local col_2 = sample_packet[3]
|
|
local scalex = (#samples-1) / w;
|
|
local amin = sample_packet[4]
|
|
local amax = sample_packet[5]
|
|
|
|
-- draw midpoint and edges
|
|
|
|
for n=1, w do
|
|
common.img_pixel_set(
|
|
img, math.floor(n), 0, this.midpoint_col);
|
|
common.img_pixel_set(
|
|
img, math.floor(half_h + lim), 0, this.midpoint_col);
|
|
common.img_pixel_set(
|
|
img, math.floor(half_h - lim), 0, this.midpoint_col);
|
|
end
|
|
|
|
-- now draw the actual waveform
|
|
|
|
if #samples > 1 then
|
|
|
|
-- (inlined rescale_value)
|
|
local adist = amax - amin;
|
|
local bmin = -lim
|
|
local bmax = lim
|
|
local bdist = bmax - bmin;
|
|
local ratio = bdist / adist;
|
|
|
|
local last = bmin + (samples[1] - amin) * ratio
|
|
|
|
for n=1, w do
|
|
local cur = bmin + (samples[math.floor(n*scalex)+1] - amin) * ratio
|
|
local top = math.floor(math.max(cur, last));
|
|
local bot = math.floor(math.min(cur, last));
|
|
for z=bot, top do
|
|
common.img_pixel_set(
|
|
img, math.floor(n), z, col_2);
|
|
end
|
|
common.img_pixel_set(img, math.floor(n), cur, col_1);
|
|
last = cur;
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
function this.push(sample_sets)
|
|
this.sample_sets = sample_sets
|
|
this.dirty = true
|
|
end
|
|
|
|
return this
|
|
|
|
end
|
|
|
|
function scene.tile9(options)
|
|
|
|
local this = scene.display_object(options)
|
|
|
|
this.tiles = options.tiles
|
|
if this.tiles == nil then error('tile9 requires a tiles image specified') end
|
|
|
|
this.tile_width, this.tile_height = common.img_get_dims(this.tiles)
|
|
this.tile_width = math.floor(this.tile_width / 3)
|
|
this.tile_height = math.floor(this.tile_height / 3)
|
|
|
|
this.use_img = true
|
|
this.dirty = true
|
|
|
|
function this.draw_update()
|
|
local w = math.ceil(this.width)
|
|
local h = math.ceil(this.height)
|
|
local img = this.img
|
|
local tw = this.tile_width
|
|
local th = this.tile_height
|
|
common.img_fill(img, 0)
|
|
-- corners
|
|
client.img_blit_to(img, this.tiles, 0, 0, tw, th, 0, 0)
|
|
client.img_blit_to(img, this.tiles, w-tw, 0, tw, th, tw*2, 0)
|
|
client.img_blit_to(img, this.tiles, 0, h-th, tw, th, 0, th*2)
|
|
client.img_blit_to(img, this.tiles, w-tw, h-th, tw, th, tw*2, th*2)
|
|
-- sides
|
|
local cap_x = 0
|
|
for ix = tw, w-tw*2, tw do
|
|
client.img_blit_to(img, this.tiles, ix, 0, tw, th, tw, 0)
|
|
client.img_blit_to(img, this.tiles, ix, h-th, tw, th, tw, th*2)
|
|
cap_x = ix
|
|
end
|
|
cap_x = cap_x + tw
|
|
|
|
local cap_y = 0
|
|
for iy = th, h-th*2, th do
|
|
client.img_blit_to(img, this.tiles, 0, iy, tw, th, 0, th)
|
|
client.img_blit_to(img, this.tiles, w-tw, iy, tw, th, tw*2, th)
|
|
cap_y = iy
|
|
end
|
|
cap_y = cap_y + th
|
|
|
|
-- middle
|
|
for ix = tw, w-tw*2, tw do
|
|
for iy = th, h-th*2, th do
|
|
client.img_blit_to(img, this.tiles, ix, iy, tw, th, tw, th)
|
|
end
|
|
end
|
|
|
|
-- fill gaps with partial tiles
|
|
|
|
client.img_blit_to(img, this.tiles, cap_x, 0, w-tw-cap_x, th, tw, 0)
|
|
client.img_blit_to(img, this.tiles, cap_x, h-th, w-tw-cap_x, th, tw, th*2)
|
|
for iy = th, h-th*2, th do
|
|
client.img_blit_to(img, this.tiles, cap_x, iy, w-tw-cap_x, th, tw, th)
|
|
end
|
|
|
|
client.img_blit_to(img, this.tiles, 0, cap_y, tw, h-th-cap_y, 0, th)
|
|
client.img_blit_to(img, this.tiles, w-tw, cap_y, tw, h-th-cap_y, tw*2, th)
|
|
for ix = tw, w-tw*2, tw do
|
|
client.img_blit_to(img, this.tiles, ix, cap_y, tw, h-th-cap_y, tw, th)
|
|
end
|
|
|
|
client.img_blit_to(img, this.tiles, cap_x, cap_y, w-tw-cap_x, h-th-cap_y, tw, th)
|
|
|
|
end
|
|
|
|
return this
|
|
|
|
end
|
|
|
|
function scene.textfield(options)
|
|
|
|
local this = scene.display_object(options)
|
|
|
|
this.wordwrap = options.wordwrap
|
|
this.autosize = options.autosize or true
|
|
this.font = options.font or font_mini
|
|
this.use_img = true
|
|
this.take_input = options.take_input
|
|
if options.take_input==nil then this.take_input = false end
|
|
|
|
-- so, we are computing the text around offset 0, 0
|
|
-- but when we go to display or collide with it, we're going to have to
|
|
-- apply the widget offsets on top.
|
|
|
|
-- TODO: cursor + text selection collision, data structure, rendering
|
|
-- TODO: compute_unwrapped should allow new lines...
|
|
|
|
function this.setter_keys.width(w)
|
|
if this.autosize == false then
|
|
rawset(this, 'width', w)
|
|
this.dirty = true
|
|
end
|
|
end
|
|
|
|
function this.setter_keys.height(h)
|
|
if this.autosize == false then
|
|
rawset(this, 'height', h)
|
|
this.dirty = true
|
|
end
|
|
end
|
|
|
|
local _text
|
|
local _ctab
|
|
local _color = options.color or 0xFF880088
|
|
if type(_color) ~= "number" then error("color "..tostring(_color).." is not a number") end
|
|
|
|
local function recalc_glyphs()
|
|
if _ctab ~= nil then
|
|
this.text_cache = this.font.compute_ctab(_ctab, 0, 0)
|
|
else
|
|
if this.wordwrap == true then
|
|
this.text_cache = this.font.compute_wordwrap(this.width,
|
|
0, 0, _color, _text)
|
|
else
|
|
this.text_cache = this.font.compute_unwrapped(0, 0,
|
|
_color, _text)
|
|
end
|
|
end
|
|
this.dirty = true
|
|
end
|
|
|
|
local function recalc_size()
|
|
recalc_glyphs()
|
|
if this.autosize then
|
|
local dim = this.font.dimensions(this.text_cache)
|
|
rawset(this, 'width', dim.width)
|
|
rawset(this, 'height', dim.height)
|
|
end
|
|
end
|
|
|
|
function this.getter_keys.text()
|
|
return _text
|
|
end
|
|
|
|
function this.setter_keys.text(str)
|
|
_ctab = nil
|
|
if _text == str then return end
|
|
_text = str
|
|
recalc_size()
|
|
end
|
|
|
|
function this.getter_keys.ctab()
|
|
return _ctab
|
|
end
|
|
|
|
function this.setter_keys.ctab(ctab)
|
|
_text = nil
|
|
|
|
-- test sameness
|
|
|
|
local same = true
|
|
if _ctab == nil or #_ctab ~= #ctab then same = false
|
|
else
|
|
local i
|
|
for i=1, #ctab do
|
|
if _ctab[i] ~= ctab then same = false break end
|
|
end
|
|
end
|
|
if same then return end
|
|
|
|
-- copy and dirtify
|
|
|
|
local tabcopy = {}
|
|
for k, v in pairs(ctab) do
|
|
table.insert(tabcopy, v)
|
|
end
|
|
_ctab = tabcopy
|
|
recalc_size()
|
|
end
|
|
|
|
function this.draw_update()
|
|
common.img_fill(this.img, 0x00000000)
|
|
this.font.print_precomputed(this.text_cache, 0, 0, this.img)
|
|
end
|
|
|
|
function this.getter_keys.color(v)
|
|
return _color
|
|
end
|
|
|
|
function this.setter_keys.color(v)
|
|
if type(v) ~= "number" then error("color "..tostring(v).." is not a number") end
|
|
_color = v
|
|
recalc_glyphs()
|
|
end
|
|
|
|
this.text = options.text or ""
|
|
if options.ctab ~= nil then this.ctab = options.ctab end
|
|
|
|
function this.on_return(key, state, modif)
|
|
this.done_typing()
|
|
end
|
|
|
|
function this.clear_keyrepeat()
|
|
this.static_alarms['key_waitbuf'] = nil
|
|
this.static_alarms['key_repeat'] = nil
|
|
this.repeating_key = nil
|
|
this.repeating_modif = nil
|
|
this.repeating_uni = nil
|
|
end
|
|
|
|
function this.done_typing()
|
|
discard_typing_state(this)
|
|
end
|
|
|
|
function this.on_key(key, state, modif, uni, dicks)
|
|
if this.take_input then
|
|
if state then
|
|
if key == SDLK_ESCAPE then
|
|
this.done_typing()
|
|
elseif key == SDLK_RETURN then
|
|
if #this.text>0 then this.buffer_register_new() end
|
|
this.on_return(key, state, modif)
|
|
elseif key == SDLK_LEFT then
|
|
this.cursor_backwards()
|
|
elseif key == SDLK_RIGHT then
|
|
this.cursor_forwards()
|
|
elseif key == SDLK_UP then
|
|
this.buffer_backwards()
|
|
elseif key == SDLK_DOWN then
|
|
this.buffer_forwards()
|
|
elseif key == SDLK_HOME then
|
|
this.cursor_to_text_start()
|
|
elseif key == SDLK_END then
|
|
this.cursor_to_text_end()
|
|
elseif key == SDLK_BACKSPACE then
|
|
this.text = gui_string_delete(this.text, this.cursor_position)
|
|
this.cursor_position = math.max(1, this.cursor_position - 1)
|
|
this.input_buffer.edit(this.text)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
this.add_listener(GE_KEY, this.on_key)
|
|
|
|
function this.on_text(text)
|
|
if this.take_input then
|
|
this.text = gui_string_append(this.text, text, this.cursor_position)
|
|
|
|
this.cursor_position = math.max(1, this.cursor_position + #text)
|
|
this.input_buffer.edit(this.text)
|
|
end
|
|
end
|
|
|
|
this.add_listener(GE_TEXT, this.on_text)
|
|
|
|
this.cursor_position = 1
|
|
this.input_buffer = collect_new_history_buf()
|
|
|
|
function this.cursor_backwards()
|
|
this.cursor_position = math.max(1, this.cursor_position - 1)
|
|
end
|
|
function this.cursor_forwards()
|
|
this.cursor_position = math.min(#this.text + 1, this.cursor_position + 1)
|
|
end
|
|
function this.cursor_to_text_start()
|
|
this.cursor_position = 1
|
|
end
|
|
function this.cursor_to_text_end()
|
|
this.cursor_position = #this.text + 1
|
|
end
|
|
|
|
function this.buffer_backwards()
|
|
this.text = this.input_buffer.prev()
|
|
this.cursor_to_text_end()
|
|
end
|
|
function this.buffer_forwards()
|
|
this.text = this.input_buffer.next()
|
|
this.cursor_to_text_end()
|
|
end
|
|
function this.buffer_register_new(text)
|
|
this.input_buffer.append()
|
|
if this.input_buffer.length() > 100 then this.input_buffer.shift() end
|
|
end
|
|
|
|
function this.get_cursor_xy()
|
|
if this.text_cache == nil or #this.text == 0 then
|
|
return {x=0, y=0}
|
|
else
|
|
this.cursor_position = math.min(this.cursor_position, #this.text + 1)
|
|
local lastrow = this.text_cache[#this.text_cache]
|
|
local lastchar = lastrow[#lastrow]
|
|
-- of lastchar: 3=x 4=y
|
|
return {x=(this.cursor_position - 1) * this.font.width,
|
|
y=lastchar[4]}
|
|
end
|
|
end
|
|
|
|
recalc_size()
|
|
|
|
return this
|
|
|
|
end
|
|
|
|
function scene.image(options)
|
|
|
|
local this = scene.display_object(options)
|
|
|
|
this._img = options.img
|
|
this.width, this.height = common.img_get_dims(this._img)
|
|
this.width, this.height = options.width or this.width, options.height or this.height
|
|
this.scalex, this.scaley = options.scalex or 1.0, options.scaley or 1.0
|
|
this.use_img = false
|
|
|
|
function this.getter_keys.width()
|
|
error("can't set the width of an image")
|
|
end
|
|
function this.getter_keys.height()
|
|
error("can't set the height of an image")
|
|
end
|
|
|
|
function this._recalc_size()
|
|
local pw, ph
|
|
pw, ph = this.width or pw, this.height or ph
|
|
rawset(this, 'width', pw)
|
|
rawset(this, 'height', ph)
|
|
this.dirty = true
|
|
end
|
|
function this.getter_keys.img()
|
|
return this._img
|
|
end
|
|
function this.setter_keys.img(img)
|
|
this._img = img
|
|
this._recalc_size()
|
|
end
|
|
function this.draw_update()
|
|
client.img_blit(this._img, this.l, this.t, this.width, this.height, 0, 0, 0xFFFFFFFF, this.scalex, this.scaley)
|
|
end
|
|
|
|
this._recalc_size()
|
|
|
|
return this
|
|
end
|
|
|
|
function scene.bone(options)
|
|
|
|
local this = scene.display_object(options)
|
|
|
|
this.z = options.z or 1
|
|
this.model = options.model or nil
|
|
this.va = options.va or nil
|
|
this.bone_idx = options.bone_idx or 0
|
|
this.rot_x = options.rot_x or 0
|
|
this.rot_y = options.rot_y or 0
|
|
this.rot_y2 = options.rot_y2 or 0
|
|
this.scale = options.scale or 1
|
|
|
|
function this.getter_keys.width() return 0 end
|
|
this.getter_keys.height = this.getter_keys.width
|
|
|
|
function this.draw_update()
|
|
if this.model ~= nil or this.va ~= nil then
|
|
-- remap pixel coordinates to (-1, 1) range
|
|
local ratio = root.height/root.width
|
|
local mx = -(this.relx/root.width*2-1)
|
|
local my = (this.rely/root.height*2-1)*ratio
|
|
this.model.render_local(
|
|
mx, my,
|
|
this.z,
|
|
this.rot_y, this.rot_x, this.rot_y2, this.scale)
|
|
end
|
|
end
|
|
|
|
return this
|
|
|
|
end
|
|
|
|
--[[
|
|
Each frame, before we start drawing, we traverse the DL tree in order to
|
|
pass in events.
|
|
|
|
Each displayobject has a "hash of lists" - one list for each event type.
|
|
Callbacks are simply stored in the list.
|
|
]]
|
|
|
|
--[[
|
|
The shared alarm records "whether it went off" this frame.
|
|
Each count of the trigger injects a SHARED_ALARM event into this frame.
|
|
]]
|
|
|
|
scene.text_cursor = scene.rect_frame{
|
|
frame_col = 0x33004488,
|
|
fill_col = 0x66004488,
|
|
align_x = 0,
|
|
align_y = 0,
|
|
x = 0,
|
|
y = 0,
|
|
visible = false
|
|
}
|
|
scene.text_cursor.add_listener(GE_SHARED_ALARM, function()
|
|
local this = scene.text_cursor
|
|
-- TODO: Figure out if gui_focus should really be global
|
|
if gui_focus ~= nil and gui_focus.font ~=nil then
|
|
|
|
local font = gui_focus.font
|
|
|
|
this.width = font.width
|
|
this.height = font.height
|
|
scene.text_cursor.set_parent(gui_focus)
|
|
|
|
local target = gui_focus.get_cursor_xy()
|
|
|
|
-- tweening
|
|
|
|
if this.visible == false then
|
|
this.x = target.x
|
|
this.y = target.y
|
|
else
|
|
this.x = this.x + (target.x - this.x) * 0.3
|
|
this.y = this.y + (target.y - this.y) * 0.3
|
|
end
|
|
|
|
this.visible = true
|
|
else
|
|
this.visible = false
|
|
scene.text_cursor.set_parent(scene.root)
|
|
end
|
|
end)
|
|
scene.text_cursor.set_parent(scene.root)
|
|
|
|
-- TEST CODE
|
|
--[[local frame = scene.rect_frame{width=320,height=320, x=width/2, y=height/2}
|
|
local frame2 = scene.rect_frame{width=32,height=32, x=0, y=0}
|
|
local frame3 = scene.rect_frame{width=32,height=32, x=64, y=96}
|
|
local text1 = scene.textfield{width=400,height=100, text="hello world"}
|
|
local bone = scene.bone{model=mdl_intel, rot_y = -0.3, rot_x = -0.4, rot_y2 = 0.2, scale = 0.5}
|
|
root.add_child(frame)
|
|
frame.add_child(text1)
|
|
frame.add_child(frame2)
|
|
frame.add_child(frame3)
|
|
frame.child_to_top(text1)
|
|
frame.add_child(bone)
|
|
|
|
-- rotate using shared alarm(accumulates 60hz frames)
|
|
|
|
local function bone_rotate(dT)
|
|
bone.rot_y = bone.rot_y + 1./6
|
|
end
|
|
bone.add_listener(GE_SHARED_ALARM, bone_rotate)]]
|
|
--[[
|
|
|
|
-- rotate using dT(passes in the raw dT value and multiplies)
|
|
|
|
local function bone_rotate_2(dT)
|
|
bone.rot_y = bone.rot_y + dT
|
|
end
|
|
bone.add_listener(GE_DELTA_TIME, bone_rotate_2)]]
|
|
|
|
return scene
|
|
|
|
end
|
|
|
|
function gui_free_scene(scene)
|
|
for k in 1, #scene.buffers, 1 do
|
|
common.img_free(buffers[k])
|
|
end
|
|
end
|
|
|
|
end
|