Skip to content

Instantly share code, notes, and snippets.

@exerro
Created February 19, 2017 21:01
Show Gist options
  • Save exerro/7ef25646c18b137b31796f32d33997d2 to your computer and use it in GitHub Desktop.
Save exerro/7ef25646c18b137b31796f32d33997d2 to your computer and use it in GitHub Desktop.
local sheets
local __debug_err_map={
}
local __debug_err_pats={
["lua.arithmetic"]={{"attempt to perform arithmetic __(%w+) on (%w+) and (%w+)","failed to %1 ${1} (%2) and ${2} (%3)"}}
}
local __debug_line_tracker={
{119,122,"main",1},
{123,141,"/sheets/src/v0.0.4/sheets",1},
{142,261,"constants",1},
{262,318,"/sheets/src/v0.0.4/sheets",20},
{319,543,".sheets.lib.class",1},
{544,544,"/sheets/src/v0.0.4/sheets",77},
{545,565,"lib.clipboard",1},
{566,566,"/sheets/src/v0.0.4/sheets",78},
{567,609,"lib.parameters",1},
{610,611,"/sheets/src/v0.0.4/sheets",79},
{612,1902,"surface2",1},
{1903,1904,"/sheets/src/v0.0.4/sheets",81},
{1905,1929,"enum.Easing",1},
{1930,1931,"/sheets/src/v0.0.4/sheets",83},
{1932,2035,"exceptions.Exception",1},
{2036,2036,"/sheets/src/v0.0.4/sheets",85},
{2037,2046,"exceptions.IncorrectParameterException",1},
{2047,2047,"/sheets/src/v0.0.4/sheets",86},
{2048,2057,"exceptions.IncorrectConstructorException",1},
{2058,2058,"/sheets/src/v0.0.4/sheets",87},
{2059,2068,"exceptions.ResourceLoadException",1},
{2069,2069,"/sheets/src/v0.0.4/sheets",88},
{2070,2079,"exceptions.ThreadRuntimeException",1},
{2080,2081,"/sheets/src/v0.0.4/sheets",89},
{2082,2145,"interfaces.ICollatedChildren",1},
{2146,2146,"/sheets/src/v0.0.4/sheets",91},
{2147,2156,"interfaces.IColoured",1},
{2157,2157,"/sheets/src/v0.0.4/sheets",92},
{2158,2292,"interfaces.IQueryable",1},
{2293,2293,"/sheets/src/v0.0.4/sheets",93},
{2294,2449,"interfaces.IChildContainer",1},
{2450,2450,"/sheets/src/v0.0.4/sheets",94},
{2451,2535,"interfaces.ITagged",1},
{2536,2536,"/sheets/src/v0.0.4/sheets",95},
{2537,2548,"interfaces.ISize",1},
{2549,2549,"/sheets/src/v0.0.4/sheets",96},
{2550,2623,"interfaces.ITimer",1},
{2624,2625,"/sheets/src/v0.0.4/sheets",97},
{2626,2640,"events.Event",1},
{2641,2641,"/sheets/src/v0.0.4/sheets",99},
{2642,2687,"events.KeyboardEvent",1},
{2688,2688,"/sheets/src/v0.0.4/sheets",100},
{2689,2700,"events.MiscEvent",1},
{2701,2701,"/sheets/src/v0.0.4/sheets",101},
{2702,2747,"events.MouseEvent",1},
{2748,2748,"/sheets/src/v0.0.4/sheets",102},
{2749,2760,"events.TextEvent",1},
{2761,2762,"/sheets/src/v0.0.4/sheets",103},
{2763,3785,"dynamic.Codegen",1},
{3786,3786,"/sheets/src/v0.0.4/sheets",105},
{3787,4156,"dynamic.DynamicValueParser",1},
{4157,4157,"/sheets/src/v0.0.4/sheets",106},
{4158,4302,"dynamic.QueryTracker",1},
{4303,4303,"/sheets/src/v0.0.4/sheets",107},
{4304,4519,"dynamic.Stream",1},
{4520,4520,"/sheets/src/v0.0.4/sheets",108},
{4521,4550,"dynamic.Transition",1},
{4551,4551,"/sheets/src/v0.0.4/sheets",109},
{4552,4661,"dynamic.Type",1},
{4662,4662,"/sheets/src/v0.0.4/sheets",110},
{4663,4929,"dynamic.Typechecking",1},
{4930,4930,"/sheets/src/v0.0.4/sheets",111},
{4931,5187,"dynamic.ValueHandler",1},
{5188,5189,"/sheets/src/v0.0.4/sheets",112},
{5190,5621,"core.Application",1},
{5622,5622,"/sheets/src/v0.0.4/sheets",114},
{5623,5797,"core.Screen",1},
{5798,5798,"/sheets/src/v0.0.4/sheets",115},
{5799,5929,"core.Sheet",1},
{5930,5932,"/sheets/src/v0.0.4/sheets",116},
{5933,5986,"core.Thread",1},
{5987,5989,"/sheets/src/v0.0.4/sheets",119},
{5990,6090,"elements.Container",1},
{6091,6093,"/sheets/src/v0.0.4/sheets",122},
{6094,6190,"interfaces.IHasText",1},
{6191,6191,"/sheets/src/v0.0.4/sheets",125},
{6192,6250,"elements.Button",1},
{6251,6256,"/sheets/src/v0.0.4/sheets",126},
{6257,6307,"elements.ClippedContainer",1},
{6308,6317,"/sheets/src/v0.0.4/sheets",132},
{6318,6447,"elements.KeyHandler",1},
{6448,6450,"/sheets/src/v0.0.4/sheets",142},
{6451,6466,"elements.Panel",1},
{6467,6477,"/sheets/src/v0.0.4/sheets",145},
{6479,6482,"main",5}
}
local function __get_src_and_line( line )
for i = 1, #__debug_line_tracker do
local t = __debug_line_tracker[i]
if line >= t[1] and line <= t[2] then
return t[3], t[4] + line - t[1]
end
end
return "unknown source", 0
end
local function __get_err_msg( src, line, err )
if __debug_err_map[src] and __debug_err_map[src][line] then
local name = __debug_err_map[src][line][1]
for i = 1, #__debug_err_pats[name] do
local data, r = err:gsub( __debug_err_pats[name][i][1], __debug_err_pats[name][i][2]:gsub( "%$%{(%d+)%}", function( n )
return __debug_err_map[src][line][tonumber( n ) + 1]
end ), 1 )
if r > 0 then
return data
end
end
end
return err
end
local ok, err = pcall( function()
do sheets={}local KeyHandler,ICollatedChildren,KeyboardEvent,Exception,Event,ITimer,class,QueryTracker,Stream,Transition,Application,IQueryable,ThreadRuntimeException,Button,MouseEvent,ClippedContainer,ISize,TableType,Easing,MiscEvent,IColoured,Thread,Container,TextEvent,parameters,Panel,ResourceLoadException,Codegen,Typechecking,UnionType,ValueHandler,IncorrectParameterException,DynamicValueParser,Sheet,IHasText,Screen,Type,IChildContainer,IncorrectConstructorException,ListType,clipboard,ITagged
event={
mouse_down=0;
mouse_up=1;
mouse_click=2;
mouse_hold=3;
mouse_drag=4;
mouse_scroll=5;
mouse_ping=6;
key_down=7;
key_up=8;
text=9;
voice=10;
paste=11;
}
alignment={
left=0;
centre=1;
center=1;
right=2;
top=3;
bottom=4;
}
colour={
transparent=0;
white=1;
orange=2;
magenta=4;
light_blue=8;
yellow=16;
lime=32;
pink=64;
grey=128;
light_grey=256;
cyan=512;
purple=1024;
blue=2048;
brown=4096;
green=8192;
red=16384;
black=32768;
}
token={
eof="eof";
string="string";
float="float";
int=TOKEN_INT;
ident=TOKEN_IDENT;
newline="newline";
symbol="symbol";
operator=TOKEN_OPERATOR;
}
class={}
local classobj=setmetatable({},{__index=class})
local supported_meta_methods={__add=true,__sub=true,__mul=true,__div=true,__mod=true,__pow=true,__unm=true,__len=true,__eq=true,__lt=true,__lte=true,__tostring=true,__concat=true}
local function _tostring(self)
return"[Class] "..self:type()
end
local function _concat(a,b)
return tostring(a)..tostring(b)
end
local function _instance_tostring(self)
return"[Instance] "..self:type()
end
local function new_super(object,super)
local super_proxy={}
if super.super then
super_proxy.super=new_super(object,super.super)
end
setmetatable(super_proxy,{__index=function(t,k)
if type(super[k])=="function"then
return function(self,...)
if self==super_proxy then
self=object
end
object.super=super_proxy.super
local v={super[k](self,...)}
object.super=super_proxy
return unpack(v)
end
else
return super[k]
end
end,__newindex=super,__tostring=function(self)
return"[Super] "..tostring(super).." of "..tostring(object)
end})
return super_proxy
end
function classobj:new(...)
local mt={__index=self,__INSTANCE=true}
local instance=setmetatable({class=self,meta=mt},mt)
if self.super then
instance.super=new_super(instance,self.super)
end
for k,v in pairs(self.meta)do
if supported_meta_methods[k]then
mt[k]=v
end
end
if mt.__tostring==_tostring then
function mt:__tostring()
return self:tostring()
end
end
function instance:type()
return self.class:type()
end
function instance:type_of(class)
return self.class:type_of(class)
end
function instance:implements(interface)
return self.class:implements(interface)
end
if not self.tostring then
instance.tostring=_instance_tostring
end
local ob=self
while ob do
if ob[ob.meta.__type]then
ob[ob.meta.__type](instance,...)
break
end
ob=ob.super
end
return instance
end
function classobj:type()
return tostring(self.meta.__type)
end
function classobj:type_of(super)
return super==self or(self.super and self.super:type_of(super))or false
end
function classobj:implements(interface)
return self.__interface_list[interface]==true
end
function class.new(name,super,...)
local implements={...}
local len=#implements
local implements_lookup={}
if type(name)~="string"then
return error("expected string class name, got "..type(name))
end
local mt={__index=classobj,__CLASS=true,__tostring=_tostring,__concat=_concat,__call=classobj.new,__type=name}
local obj=setmetatable({meta=mt,__interface_list=implements_lookup},mt)
if super then
obj.super=super
obj.meta.__index=super
for interface in pairs(super.__interface_list)do
implements_lookup[interface]=true
end
end
for i=1,len do
implements_lookup[implements[i]]=true
for k,v in pairs(implements[i])do
if k~="__interface_list"then
obj[k]=v
end
end
for interface in pairs(implements[i].__interface_list)do
implements_lookup[interface]=true
end
end
return function(t)
for k,v in pairs(t)do
obj[k]=v
end
return obj
end
end
function class.type(object)
local _type=type(object)
if _type=="table"then
pcall(function()
local mt=getmetatable(object)
_type=((mt.__CLASS or mt.__INSTANCE)and object:type())or _type
end)
end
return _type
end
function class.type_of(object,class)
if type(object)=="table"then
local ok,v=pcall(function()
return getmetatable(object).__CLASS or getmetatable(object).__INSTANCE or error()
end)
return ok and object:type_of(class)
end
return false
end
function class.is_class(object)
return pcall(function()if not getmetatable(object).__CLASS then error()end end),nil
end
function class.is_instance(object)
return pcall(function()if not getmetatable(object).__INSTANCE then error()end end),nil
end
setmetatable(class,{
__call=class.new;
})
function class.new_interface(name,...)
local implements={...}
local implements_lookup={}
local obj={__interface_list=implements_lookup}
local len=#implements
for i=1,len do
implements_lookup[implements[i]]=true
for k,v in pairs(implements[i])do
if k~="__interface_list"then
obj[k]=v
end
end
for interface in pairs(implements[i].__interface_list)do
implements_lookup[interface]=true
end
end
return function(t)
for k,v in pairs(t)do
obj[k]=v
end
return obj
end
end
function class.new_enum(name)
return function(t)
return setmetatable({},{__index=t,__newindex=function(t,k,v)
return error("attempt to set enum index '"..tostring(k).."'" )
end } )
end
end
local c = {}
clipboard = {}
function clipboard.put( modes )
parameters.check( 1, "modes", "table", modes )
c = modes
end
function clipboard.get( mode )
parameters.check( 1, "mode", "string", mode )
return c[mode]
end
function clipboard.clear()
c = {}
end
parameters = {}
function parameters.check_constructor( _class, argc, ... )
local args = { ... }
for i = 1, argc * 3, 3 do
local name = args[i]
local expected_type = args[i + 1]
local value = args[i + 2]
if type( expected_type ) == "string" then
if type( value ) ~= expected_type then
Exception.throw( IncorrectConstructorException, _class:type() .. " expects " .. expected_type .. " " .. name .. " when created, got " .. class.type( value ), 4 )
end
else
if not class.type_of( value, expected_type ) then
Exception.throw( IncorrectConstructorException, _class:type() .. " expects " .. expected_type:type() .. " " .. name .. " when created, got " .. class.type( value ), 4 )
end
end
end
end
function parameters.check( argc, ... )
local args = { ... }
for i = 1, argc * 3, 3 do
local name = args[i]
local expected_type = args[i + 1]
local value = args[i + 2]
if type( expected_type ) == "string" then
if type( value ) ~= expected_type then
Exception.throw( IncorrectParameterException, "expected " .. expected_type .. " " .. name .. ", got " .. class.type( value ), 3 )
end
else
if not class.type_of( value, expected_type ) then
Exception.throw( IncorrectParameterException, "expected " .. expected_type:type() .. " " .. name .. ", got " .. class.type( value ), 3 )
end
end
end
end
surface = { } do
--[[
Surface version 2.0.0
The MIT License (MIT)
Copyright (c) 2017 CrazedProgrammer
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
table: 19526e4 without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
local surf = { }
surface.surf = surf
local table_concat, math_floor, math_atan2 = table.concat, math.floor, math.atan2
local _cc_color_to_hex, _cc_hex_to_color = { }, { }
for i = 0, 15 do
_cc_color_to_hex[2 ^ i] = string.format("%01x", i)
_cc_hex_to_color[string.format("%01x", i)] = 2 ^ i
end
local _chars = { }
for i = 0, 255 do
_chars[i] = string.char(i)
end
local _eprc, _esin, _ecos = 20, { }, { }
for i = 0, _eprc - 1 do
_esin[i + 1] = (1 - math.sin(i / _eprc * math.pi * 2)) / 2
_ecos[i + 1] = (1 + math.cos(i / _eprc * math.pi * 2)) / 2
end
local _steps, _palette, _rgbpal, _palr, _palg, _palb = 16
local function calcStack(stack, width, height)
local ox, oy, cx, cy, cwidth, cheight = 0, 0, 0, 0, width, height
for i = 1, #stack do
ox = ox + stack[i].ox
oy = oy + stack[i].oy
cx = cx + stack[i].x
cy = cy + stack[i].y
cwidth = stack[i].width
cheight = stack[i].height
end
return ox, oy, cx, cy, cwidth, cheight
end
local function clipRect(x, y, width, height, cx, cy, cwidth, cheight)
if x < cx then
width = width + x - cx
x = cx
end
if y < cy then
height = height + y - cy
y = cy
end
if x + width > cx + cwidth then
width = cwidth + cx - x
end
if y + height > cy + cheight then
height = cheight + cy - y
end
return x, y, width, height
end
function surface.create(width, height, b, t, c)
local surface = setmetatable({ }, {__index = surface.surf})
surface.width = width
surface.height = height
surface.buffer = { }
surface.overwrite = false
surface.stack = { }
surface.ox, surface.oy, surface.cx, surface.cy, surface.cwidth, surface.cheight = calcStack(surface.stack, width, height)
-- force array indeces instead of hashed indices
local buffer = surface.buffer
for i = 1, width * height * 3, 3 do
buffer[i] = b or false
buffer[i + 1] = t or false
buffer[i + 2] = c or false
end
buffer[width * height * 3 + 1] = false
if not b then
for i = 1, width * height * 3, 3 do
buffer[i] = b
end
end
if not t then
for i = 2, width * height * 3, 3 do
buffer[i] = t
end
end
if not c then
for i = 3, width * height * 3, 3 do
buffer[i] = c
end
end
return surface
end
function surf:output(output, x, y, sx, sy, swidth, sheight)
output = output or (term or gpu)
if love then output = output or love.graphics end
x = x or 0
y = y or 0
sx = sx or 0
sy = sy or 0
swidth = swidth or self.width
sheight = sheight or self.height
sx, sy, swidth, sheight = clipRect(sx, sy, swidth, sheight, 0, 0, self.width, self.height)
local buffer = self.buffer
local bwidth = self.width
local xoffset, yoffset, idx
if output.blit and output.setCursorPos then
-- CC
local cmd, str, text, back = { }, { }, { }, { }
for j = 0, sheight - 1 do
yoffset = (j + sy) * bwidth + sx
for i = 0, swidth - 1 do
xoffset = (yoffset + i) * 3
idx = i + 1
str[idx] = buffer[xoffset + 3] or " "
text[idx] = _cc_color_to_hex[buffer[xoffset + 2] or 1]
back[idx] = _cc_color_to_hex[buffer[xoffset + 1] or 32768]
end
output.setCursorPos(x + 1, y + j + 1)
output.blit(table_concat(str), table_concat(text), table_concat(back))
end
elseif output.write and output.setCursorPos and output.setTextColor and output.setBackgroundColor then
-- CC pre-1.76
local str, b, t, pb, pt = { }
for j = 0, sheight - 1 do
output.setCursorPos(x + 1, y + j + 1)
yoffset = (j + sy) * bwidth + sx
for i = 0, swidth - 1 do
xoffset = (yoffset + i) * 3
pb = buffer[xoffset + 1] or 32768
pt = buffer[xoffset + 2] or 1
if pb ~= b then
if #str ~= 0 then
output.write(table_concat(str))
str = { }
end
b = pb
output.setBackgroundColor(b)
end
if pt ~= t then
if #str ~= 0 then
output.write(table_concat(str))
str = { }
end
t = pt
output.setTextColor(t)
end
str[#str + 1] = buffer[xoffset + 3] or " "
end
output.write(table_concat(str))
str = { }
end
elseif output.blitPixels then
-- Riko 4
local pixels = { }
for j = 0, sheight - 1 do
yoffset = (j + sy) * bwidth + sx
for i = 0, swidth - 1 do
pixels[j * swidth + i + 1] = buffer[(yoffset + i) * 3 + 1] or 0
end
end
output.blitPixels(x, y, swidth, sheight, pixels)
elseif output.points and output.setColor then
-- Love2D
local pos, r, g, b, pr, pg, pb = { }
for j = 0, sheight - 1 do
yoffset = (j + sy) * bwidth + sx
for i = 0, swidth - 1 do
xoffset = (yoffset + i) * 3
pr = buffer[xoffset + 1]
pg = buffer[xoffset + 2]
pb = buffer[xoffset + 3]
if pr ~= r or pg ~= g or pb ~= b then
if #pos ~= 0 then
output.setColor((r or 0) * 255, (g or 0) * 255, (b or 0) * 255, (r or g or b) and 255 or 0)
output.points(pos)
end
r, g, b = pr, pg, pb
pos = { }
end
pos[#pos + 1] = i + x
pos[#pos + 1] = j + y
end
end
elseif output.drawPixel then
-- Redirection arcade (gpu)
-- todo: add image:write support for extra performance
local px = output.drawPixel
for j = 0, sheight - 1 do
for i = 0, swidth - 1 do
px(x + i, y + j, buffer[((j + sy) * bwidth + (i + sx)) * 3 + 1] or 0)
end
end
else
error("unsupported output object")
end
end
function surf:push(x, y, width, height, nooffset)
x, y = x + self.ox, y + self.oy
local ox, oy = nooffset and self.ox or x, nooffset and self.oy or y
x, y, width, height = clipRect(x, y, width, height, self.cx, self.cy, self.cwidth, self.cheight)
self.stack[#self.stack + 1] = {ox = ox - self.ox, oy = oy - self.oy, x = x - self.cx, y = y - self.cy, width = width, height = height}
self.ox, self.oy, self.cx, self.cy, self.cwidth, self.cheight = calcStack(self.stack, self.width, self.height)
end
function surf:pop()
if #self.stack == 0 then
error("no stencil to pop")
end
self.stack[#self.stack] = nil
self.ox, self.oy, self.cx, self.cy, self.cwidth, self.cheight = calcStack(self.stack, self.width, self.height)
end
function surf:copy()
local surface = setmetatable({ }, {__index = surface.surf})
for k, v in pairs(self) do
surface[k] = v
end
surface.buffer = { }
for i = 1, self.width * self.height * 3 + 1 do
surface.buffer[i] = false
end
for i = 1, self.width * self.height * 3 do
surface.buffer[i] = self.buffer[i]
end
surface.stack = { }
for i = 1, #self.stack do
surface.stack[i] = self.stack[i]
end
return surface
end
function surf:clear(b, t, c)
local xoffset, yoffset
for j = 0, self.cheight - 1 do
yoffset = (j + self.cy) * self.width + self.cx
for i = 0, self.cwidth - 1 do
xoffset = (yoffset + i) * 3
self.buffer[xoffset + 1] = b
self.buffer[xoffset + 2] = t
self.buffer[xoffset + 3] = c
end
end
end
function surf:drawPixel(x, y, b, t, c)
x, y = x + self.ox, y + self.oy
local idx
if x >= self.cx and x < self.cx + self.cwidth and y >= self.cy and y < self.cy + self.cheight then
idx = (y * self.width + x) * 3
if b or self.overwrite then
self.buffer[idx + 1] = b
end
if t or self.overwrite then
self.buffer[idx + 2] = t
end
if c or self.overwrite then
self.buffer[idx + 3] = c
end
end
end
function surf:drawString(x, y, str, b, t)
x, y = x + self.ox, y + self.oy
local sx = x
local insidey = y >= self.cy and y < self.cy + self.cheight
local idx
local lowerxlim = self.cx
local upperxlim = self.cx + self.cwidth
local writeb = b or self.overwrite
local writet = t or self.overwrite
for i = 1, #str do
local c = str:sub(i, i)
if c == "\n" then
x = sx
y = y + 1
if insidey then
if y >= self.cy + self.cheight then
return
end
else
insidey = y >= self.cy
end
else
idx = (y * self.width + x) * 3
if x >= lowerxlim and x < upperxlim and insidey then
if writeb then
self.buffer[idx + 1] = b
end
if writet then
self.buffer[idx + 2] = t
end
self.buffer[idx + 3] = c
end
x = x + 1
end
end
end
-- You can remove any of these components
function surface.load(strpath, isstr)
local data = strpath
if not isstr then
local handle = io.open(strpath, "rb")
if not handle then return end
chars = { }
local byte = handle:read(1)
while byte do
chars[#chars + 1] = _chars[byte]
byte = handle:read(1)
end
handle:close()
data = table_concat(chars)
end
if data:sub(1, 3) == "RIF" then
-- Riko 4 image format
local width, height = data:byte(4) * 256 + data:byte(5), data:byte(6) * 256 + data:byte(7)
local surf = surface.create(width, height)
local buffer = surf.buffer
local upper, byte = 8, false
local byte = data:byte(index)
for j = 0, height - 1 do
for i = 0, height - 1 do
if not upper then
buffer[(j * width + i) * 3 + 1] = math_floor(byte / 16)
else
buffer[(j * width + i) * 3 + 1] = byte % 16
index = index + 1
data = data:byte(index)
end
upper = not upper
end
end
return surf
elseif data:sub(1, 2) == "BM" then
-- BMP format
local width = data:byte(0x13) + data:byte(0x14) * 256
local height = data:byte(0x17) + data:byte(0x18) * 256
if data:byte(0xF) ~= 0x28 or data:byte(0x1B) ~= 1 or data:byte(0x1D) ~= 0x18 then
error("unsupported bmp format, only uncompressed 24-bit rgb is supported.")
end
local offset, linesize = 0x36, math.ceil((width * 3) / 4) * 4
local surf = surface.create(width, height)
local buffer = surf.buffer
for j = 0, height - 1 do
for i = 0, width - 1 do
buffer[(j * width + i) * 3 + 1] = data:byte((height - j - 1) * linesize + i * 3 + offset + 3) / 255
buffer[(j * width + i) * 3 + 2] = data:byte((height - j - 1) * linesize + i * 3 + offset + 2) / 255
buffer[(j * width + i) * 3 + 3] = data:byte((height - j - 1) * linesize + i * 3 + offset + 1) / 255
end
end
return surf
elseif data:find("\30") then
-- NFT format
local width, height, lwidth = 0, 1, 0
for i = 1, #data do
if data:byte(i) == 10 then -- newline
height = height + 1
if lwidth > width then
width = lwidth
end
lwidth = 0
elseif data:byte(i) == 30 or data:byte(i) == 31 then -- color control
lwidth = lwidth - 1
elseif data:byte(i) ~= 13 then -- not carriage return
lwidth = lwidth + 1
end
end
if data:byte(#data) == 10 then
height = height - 1
end
local surf = surface.create(width, height)
local buffer = surf.buffer
local index, x, y, b, t = 1, 0, 0
while index <= #data do
if data:byte(index) == 10 then
x, y = 0, y + 1
elseif data:byte(index) == 30 then
index = index + 1
b = _cc_hex_to_color[data:sub(index, index)]
elseif data:byte(index) == 31 then
index = index + 1
t = _cc_hex_to_color[data:sub(index, index)]
elseif data:byte(index) ~= 13 then
buffer[(y * width + x) * 3 + 1] = b
buffer[(y * width + x) * 3 + 2] = t
if b or t then
buffer[(y * width + x) * 3 + 3] = data:sub(index, index)
elseif data:sub(index, index) ~= " " then
buffer[(y * width + x) * 3 + 3] = data:sub(index, index)
end
x = x + 1
end
index = index + 1
end
return surf
else
-- NFP format
local width, height, lwidth = 0, 1, 0
for i = 1, #data do
if data:byte(i) == 10 then -- newline
height = height + 1
if lwidth > width then
width = lwidth
end
lwidth = 0
elseif data:byte(i) ~= 13 then -- not carriage return
lwidth = lwidth + 1
end
end
if data:byte(#data) == 10 then
height = height - 1
end
local surf = surface.create(width, height)
local buffer = surf.buffer
local x, y = 0, 0
for i = 1, #data do
if data:byte(i) == 10 then
x, y = 0, y + 1
elseif data:byte(i) ~= 13 then
buffer[(y * width + x) * 3 + 1] = _cc_hex_to_color[data:sub(i, i)]
x = x + 1
end
end
return surf
end
end
function surf:save(file, format)
format = format or "nfp"
local data = { }
if format == "nfp" then
for j = 0, self.height - 1 do
for i = 0, self.width - 1 do
data[#data + 1] = _cc_color_to_hex[self.buffer[(j * self.width + i) * 3 + 1]] or " "
end
data[#data + 1] = "\n"
end
elseif format == "nft" then
for j = 0, self.height - 1 do
local b, t, pb, pt
for i = 0, self.width - 1 do
pb = self.buffer[(j * self.width + i) * 3 + 1]
pt = self.buffer[(j * self.width + i) * 3 + 2]
if pb ~= b then
data[#data + 1] = "\30"..(_cc_color_to_hex[pb] or " ")
b = pb
end
if pt ~= t then
data[#data + 1] = "\31"..(_cc_color_to_hex[pt] or " ")
t = pt
end
data[#data + 1] = self.buffer[(j * self.width + i) * 3 + 3] or " "
end
data[#data + 1] = "\n"
end
elseif format == "rif" then
data[1] = "RIF"
data[2] = string.char(math_floor(self.width / 256), self.width % 256)
data[3] = string.char(math_floor(self.height / 256), self.height % 256)
local byte, upper, c = 0, false
for j = 0, self.width - 1 do
for i = 0, self.height - 1 do
c = self.buffer[(j * self.width + i) * 3 + 1] or 0
if not upper then
byte = c * 16
else
byte = byte + c
data[#data + 1] = string.char(byte)
end
upper = not upper
end
end
if upper then
data[#data + 1] = string.char(byte)
end
elseif format == "bmp" then
data[1] = "BM"
data[2] = string.char(0, 0, 0, 0) -- file size, change later
data[3] = string.char(0, 0, 0, 0, 0x36, 0, 0, 0, 0x28, 0, 0, 0)
data[4] = string.char(self.width % 256, math_floor(self.width / 256), 0, 0)
data[5] = string.char(self.height % 256, math_floor(self.height / 256), 0, 0)
data[6] = string.char(1, 0, 0x18, 0, 0, 0, 0, 0)
data[7] = string.char(0, 0, 0, 0) -- pixel data size, change later
data[8] = string.char(0x13, 0x0B, 0, 0, 0x13, 0x0B, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
local padchars = math.ceil((self.width * 3) / 4) * 4 - self.width * 3
for j = 0, self.height - 1 do
for i = 0, self.width - 1 do
data[#data + 1] = string.char((self.buffer[(j * self.width + i) * 3 + 1] or 0) * 255)
data[#data + 1] = string.char((self.buffer[(j * self.width + i) * 3 + 2] or 0) * 255)
data[#data + 1] = string.char((self.buffer[(j * self.width + i) * 3 + 3] or 0) * 255)
end
data[#data + 1] = ("\0"):rep(padchars)
end
local size = #table_concat(data)
data[2] = string.char(size % 256, math_floor(size / 256) % 256, math_floor(size / 65536), 0)
size = size - 54
data[7] = string.char(size % 256, math_floor(size / 256) % 256, math_floor(size / 65536), 0)
else
error("format not supported")
end
data = table_concat(data)
if file then
local handle = io.open(file, "wb")
for i = 1, #data do
handle:write(data:byte(i))
end
handle:close()
end
return data
end
function surf:drawLine(x1, y1, x2, y2, b, t, c)
if x1 == x2 then
x1, y1, x2, y2 = x1 + self.ox, y1 + self.oy, x2 + self.ox, y2 + self.oy
if x1 < self.cx or x1 >= self.cx + self.cwidth then return end
if y2 < y1 then
local temp = y1
y1 = y2
y2 = temp
end
if y1 < self.cy then y1 = self.cy end
if y2 >= self.cy + self.cheight then y2 = self.cy + self.cheight - 1 end
if b or self.overwrite then
for j = y1, y2 do
self.buffer[(j * self.width + x1) * 3 + 1] = b
end
end
if t or self.overwrite then
for j = y1, y2 do
self.buffer[(j * self.width + x1) * 3 + 2] = t
end
end
if c or self.overwrite then
for j = y1, y2 do
self.buffer[(j * self.width + x1) * 3 + 3] = c
end
end
elseif y1 == y2 then
x1, y1, x2, y2 = x1 + self.ox, y1 + self.oy, x2 + self.ox, y2 + self.oy
if y1 < self.cy or y1 >= self.cy + self.cheight then return end
if x2 < x1 then
local temp = x1
x1 = x2
x2 = temp
end
if x1 < self.cx then x1 = self.cx end
if x2 >= self.cx + self.cwidth then x2 = self.cx + self.cwidth - 1 end
if b or self.overwrite then
for i = x1, x2 do
self.buffer[(y1 * self.width + i) * 3 + 1] = b
end
end
if t or self.overwrite then
for i = x1, x2 do
self.buffer[(y1 * self.width + i) * 3 + 2] = t
end
end
if c or self.overwrite then
for i = x1, x2 do
self.buffer[(y1 * self.width + i) * 3 + 3] = c
end
end
else
local delta_x = x2 - x1
local ix = delta_x > 0 and 1 or -1
delta_x = 2 * math.abs(delta_x)
local delta_y = y2 - y1
local iy = delta_y > 0 and 1 or -1
delta_y = 2 * math.abs(delta_y)
self:drawPixel(x1, y1, b, t, c)
if delta_x >= delta_y then
local error = delta_y - delta_x / 2
while x1 ~= x2 do
if (error >= 0) and ((error ~= 0) or (ix > 0)) then
error = error - delta_x
y1 = y1 + iy
end
error = error + delta_y
x1 = x1 + ix
self:drawPixel(x1, y1, b, t, c)
end
else
local error = delta_x - delta_y / 2
while y1 ~= y2 do
if (error >= 0) and ((error ~= 0) or (iy > 0)) then
error = error - delta_y
x1 = x1 + ix
end
error = error + delta_x
y1 = y1 + iy
self:drawPixel(x1, y1, b, t, c)
end
end
end
end
function surf:drawRect(x, y, width, height, b, t, c)
self:drawLine(x, y, x + width - 1, y, b, t, c)
self:drawLine(x, y, x, y + height - 1, b, t, c)
self:drawLine(x + width - 1, y, x + width - 1, y + height - 1, b, t, c)
self:drawLine(x, y + height - 1, x + width - 1, y + height - 1, b, t, c)
end
function surf:fillRect(x, y, width, height, b, t, c)
x, y, width, height = clipRect(x + self.ox, y + self.oy, width, height, self.cx, self.cy, self.cwidth, self.cheight)
if b or self.overwrite then
for j = 0, height - 1 do
for i = 0, width - 1 do
self.buffer[((j + y) * self.width + i + x) * 3 + 1] = b
end
end
end
if t or self.overwrite then
for j = 0, height - 1 do
for i = 0, width - 1 do
self.buffer[((j + y) * self.width + i + x) * 3 + 2] = t
end
end
end
if c or self.overwrite then
for j = 0, height - 1 do
for i = 0, width - 1 do
self.buffer[((j + y) * self.width + i + x) * 3 + 3] = c
end
end
end
end
function surf:drawTriangle(x1, y1, x2, y2, x3, y3, b, t, c)
self:drawLine(x1, y1, x2, y2, b, t, c)
self:drawLine(x2, y2, x3, y3, b, t, c)
self:drawLine(x3, y3, x1, y1, b, t, c)
end
function surf:fillTriangle(x1, y1, x2, y2, x3, y3, b, t, c)
if y1 > y2 then
local tempx, tempy = x1, y1
x1, y1 = x2, y2
x2, y2 = tempx, tempy
end
if y1 > y3 then
local tempx, tempy = x1, y1
x1, y1 = x3, y3
x3, y3 = tempx, tempy
end
if y2 > y3 then
local tempx, tempy = x2, y2
x2, y2 = x3, y3
x3, y3 = tempx, tempy
end
if y1 == y2 and x1 > x2 then
local temp = x1
x1 = x2
x2 = temp
end
if y2 == y3 and x2 > x3 then
local temp = x2
x2 = x3
x3 = temp
end
local x4, y4
if x1 <= x2 then
x4 = x1 + (y2 - y1) / (y3 - y1) * (x3 - x1)
y4 = y2
local tempx, tempy = x2, y2
x2, y2 = x4, y4
x4, y4 = tempx, tempy
else
x4 = x1 + (y2 - y1) / (y3 - y1) * (x3 - x1)
y4 = y2
end
local finvslope1 = (x2 - x1) / (y2 - y1)
local finvslope2 = (x4 - x1) / (y4 - y1)
local linvslope1 = (x3 - x2) / (y3 - y2)
local linvslope2 = (x3 - x4) / (y3 - y4)
local xstart, xend, dxstart, dxend
for y = math.ceil(y1 + 0.5) - 0.5, math.floor(y3 - 0.5) + 0.5, 1 do
if y <= y2 then -- first half
xstart = x1 + finvslope1 * (y - y1)
xend = x1 + finvslope2 * (y - y1)
else -- second half
xstart = x3 - linvslope1 * (y3 - y)
xend = x3 - linvslope2 * (y3 - y)
end
dxstart, dxend = math.ceil(xstart - 0.5), math.floor(xend - 0.5)
if dxstart <= dxend then
self:drawLine(dxstart, y - 0.5, dxend, y - 0.5, b, t, c)
end
end
end
function surf:drawEllipse(x, y, width, height, b, t, c)
for i = 0, _eprc - 1 do
self:drawLine(math_floor(x + _ecos[i + 1] * (width - 1) + 0.5), math_floor(y + _esin[i + 1] * (height - 1) + 0.5), math_floor(x + _ecos[(i + 1) % _eprc + 1] * (width - 1) + 0.5), math_floor(y + _esin[(i + 1) % _eprc + 1] * (height - 1) + 0.5), b, t, c)
end
end
function surf:fillEllipse(x, y, width, height, b, t, c)
x, y = x + self.ox, y + self.oy
for j = 0, height - 1 do
for i = 0, width - 1 do
if ((i + 0.5) / width * 2 - 1) ^ 2 + ((j + 0.5) / height * 2 - 1) ^ 2 <= 1 then
if b or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 1] = b
end
if t or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 2] = t
end
if c or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 3] = c
end
end
end
end
end
function surf:drawArc(x, y, width, height, fromangle, toangle, b, t, c)
if fromangle > toangle then
local temp = fromangle
fromangle = toangle
temp = toangle
end
fromangle = math_floor(fromangle / math.pi / 2 * _eprc + 0.5)
toangle = math_floor(toangle / math.pi / 2 * _eprc + 0.5) - 1
for j = fromangle, toangle do
local i = j % _eprc
self:drawLine(math_floor(x + _ecos[i + 1] * (width - 1) + 0.5), math_floor(y + _esin[i + 1] * (height - 1) + 0.5), math_floor(x + _ecos[(i + 1) % _eprc + 1] * (width - 1) + 0.5), math_floor(y + _esin[(i + 1) % _eprc + 1] * (height - 1) + 0.5), b, t, c)
end
end
function surf:fillArc(x, y, width, height, fromangle, toangle, b, t, c)
x, y = x + self.ox, y + self.oy
if fromangle > toangle then
local temp = fromangle
fromangle = toangle
temp = toangle
end
local diff = toangle - fromangle
fromangle = fromangle % (math.pi * 2)
local fx, fy, dir
for j = 0, height - 1 do
for i = 0, width - 1 do
fx, fy = (i + 0.5) / width * 2 - 1, (j + 0.5) / height * 2 - 1
dir = math_atan2(-fy, fx) % (math.pi * 2)
if fx ^ 2 + fy ^ 2 <= 1 and ((dir >= fromangle and dir - fromangle <= diff) or (dir <= (fromangle + diff) % (math.pi * 2))) then
if b or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 1] = b
end
if t or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 2] = t
end
if c or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 3] = c
end
end
end
end
end
function surf:drawSurface(surf2, x, y, width, height, sx, sy, swidth, sheight)
x, y, width, height, sx, sy, swidth, sheight = x + self.ox, y + self.oy, width or surf2.width, height or surf2.height, sx or 0, sy or 0, swidth or surf2.width, sheight or surf2.height
if width == swidth and height == sheight then
local nx, ny
nx, ny, width, height = clipRect(x, y, width, height, self.cx, self.cy, self.cwidth, self.cheight)
swidth, sheight = width, height
if nx > x then
sx = sx + nx - x
x = nx
end
if ny > y then
sy = sy + ny - y
y = ny
end
nx, ny, swidth, sheight = clipRect(sx, sy, swidth, sheight, 0, 0, surf2.width, surf2.height)
width, height = swidth, sheight
if nx > sx then
x = x + nx - sx
sx = nx
end
if ny > sy then
y = y + ny - sy
sy = ny
end
local b, t, c
for j = 0, height - 1 do
for i = 0, width - 1 do
b = surf2.buffer[((j + sy) * surf2.width + i + sx) * 3 + 1]
t = surf2.buffer[((j + sy) * surf2.width + i + sx) * 3 + 2]
c = surf2.buffer[((j + sy) * surf2.width + i + sx) * 3 + 3]
if b or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 1] = b
end
if t or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 2] = t
end
if c or self.overwrite then
self.buffer[((j + y) * self.width + i + x) * 3 + 3] = c
end
end
end
else
local hmirror, vmirror = false, false
if width < 0 then
hmirror = true
x = x + width
end
if height < 0 then
vmirror = true
y = y + height
end
if swidth < 0 then
hmirror = not hmirror
sx = sx + swidth
end
if sheight < 0 then
vmirror = not vmirror
sy = sy + sheight
end
width, height, swidth, sheight = math.abs(width), math.abs(height), math.abs(swidth), math.abs(sheight)
local xscale, yscale, px, py, ssx, ssy, b, t, c = swidth / width, sheight / height
for j = 0, height - 1 do
for i = 0, width - 1 do
px, py = math_floor((i + 0.5) * xscale), math_floor((j + 0.5) * yscale)
if hmirror then
ssx = x + width - i - 1
else
ssx = i + x
end
if vmirror then
ssy = y + height - j - 1
else
ssy = j + y
end
if ssx >= self.cx and ssx < self.cx + self.cwidth and ssy >= self.cy and ssy < self.cy + self.cheight and px >= 0 and px < surf2.width and py >= 0 and py < surf2.height then
b = surf2.buffer[(py * surf2.width + px) * 3 + 1]
t = surf2.buffer[(py * surf2.width + px) * 3 + 2]
c = surf2.buffer[(py * surf2.width + px) * 3 + 3]
if b or self.overwrite then
self.buffer[(ssy * self.width + ssx) * 3 + 1] = b
end
if t or self.overwrite then
self.buffer[(ssy * self.width + ssx) * 3 + 2] = t
end
if c or self.overwrite then
self.buffer[(ssy * self.width + ssx) * 3 + 3] = c
end
end
end
end
end
end
function surf:drawSurfaceRotated(surf2, x, y, ox, oy, angle)
local sin, cos, sx, sy, px, py = math.sin(angle), math.cos(angle)
for j = math.floor(-surf2.height * 0.75), math.ceil(surf2.height * 0.75) do
for i = math.floor(-surf2.width * 0.75), math.ceil(surf2.width * 0.75) do
sx, sy, px, py = x + i, y + j, math_floor(cos * (i + 0.5) - sin * (j + 0.5) + ox), math_floor(sin * (i + 0.5) + cos * (j + 0.5) + oy)
if sx >= self.cx and sx < self.cx + self.cwidth and sy >= self.cy and sy < self.cy + self.cheight and px >= 0 and px < surf2.width and py >= 0 and py < surf2.height then
b = surf2.buffer[(py * surf2.width + px) * 3 + 1]
t = surf2.buffer[(py * surf2.width + px) * 3 + 2]
c = surf2.buffer[(py * surf2.width + px) * 3 + 3]
if b or self.overwrite then
self.buffer[(sy * self.width + sx) * 3 + 1] = b
end
if t or self.overwrite then
self.buffer[(sy * self.width + sx) * 3 + 2] = t
end
if c or self.overwrite then
self.buffer[(sy * self.width + sx) * 3 + 3] = c
end
end
end
end
end
function surf:drawSurfacesInterlaced(surfs, x, y, step)
x, y, step = x + self.ox, y + self.oy, step or 0
local width, height = surfs[1].width, surfs[1].height
for i = 2, #surfs do
if surfs[i].width ~= width or surfs[i].height ~= height then
error("surfaces should be the same size")
end
end
local sx, sy, swidth, sheight, index, b, t, c = clipRect(x, y, width, height, self.cx, self.cy, self.cwidth, self.cheight)
for j = sy, sy + sheight - 1 do
for i = sx, sx + swidth - 1 do
index = (i + j + step) % #surfs + 1
b = surfs[index].buffer[((j - sy) * surfs[index].width + i - sx) * 3 + 1]
t = surfs[index].buffer[((j - sy) * surfs[index].width + i - sx) * 3 + 2]
c = surfs[index].buffer[((j - sy) * surfs[index].width + i - sx) * 3 + 3]
if b or self.overwrite then
self.buffer[(j * self.width + i) * 3 + 1] = b
end
if t or self.overwrite then
self.buffer[(j * self.width + i) * 3 + 2] = t
end
if c or self.overwrite then
self.buffer[(j * self.width + i) * 3 + 3] = c
end
end
end
end
function surf:drawSurfaceSmall(surf2, x, y)
x, y = x + self.ox, y + self.oy
if surf2.width % 2 ~= 0 or surf2.height % 3 ~= 0 then
error("surface width must be a multiple of 2 and surface height a multiple of 3")
end
local sub, char, c1, c2, c3, c4, c5, c6 = 32768
for j = 0, surf2.height / 3 - 1 do
for i = 0, surf2.width / 2 - 1 do
if i + x >= self.cx and i + x < self.cx + self.cwidth and j + y >= self.cy and j + y < self.cy + self.cheight then
char, c1, c2, c3, c4, c5, c6 = 0,
surf2.buffer[((j * 3) * surf2.width + i * 2) * 3 + 1],
surf2.buffer[((j * 3) * surf2.width + i * 2 + 1) * 3 + 1],
surf2.buffer[((j * 3 + 1) * surf2.width + i * 2) * 3 + 1],
surf2.buffer[((j * 3 + 1) * surf2.width + i * 2 + 1) * 3 + 1],
surf2.buffer[((j * 3 + 2) * surf2.width + i * 2) * 3 + 1],
surf2.buffer[((j * 3 + 2) * surf2.width + i * 2 + 1) * 3 + 1]
if c1 ~= c6 then
sub = c1
char = 1
end
if c2 ~= c6 then
sub = c2
char = char + 2
end
if c3 ~= c6 then
sub = c3
char = char + 4
end
if c4 ~= c6 then
sub = c4
char = char + 8
end
if c5 ~= c6 then
sub = c5
char = char + 16
end
self.buffer[((j + y) * self.width + i + x) * 3 + 1] = c6
self.buffer[((j + y) * self.width + i + x) * 3 + 2] = sub
self.buffer[((j + y) * self.width + i + x) * 3 + 3] = _chars[128 + char]
end
end
end
end
function surf:flip(horizontal, vertical)
local ox, oy, nx, ny, tb, tt, tc
if horizontal then
for i = 0, math.ceil(self.cwidth / 2) - 1 do
for j = 0, self.cheight - 1 do
ox, oy, nx, ny = i + self.cx, j + self.cy, self.cx + self.cwidth - i - 1, j + self.cy
tb = self.buffer[(oy * self.width + ox) * 3 + 1]
tt = self.buffer[(oy * self.width + ox) * 3 + 2]
tc = self.buffer[(oy * self.width + ox) * 3 + 3]
self.buffer[(oy * self.width + ox) * 3 + 1] = self.buffer[(ny * self.width + nx) * 3 + 1]
self.buffer[(oy * self.width + ox) * 3 + 2] = self.buffer[(ny * self.width + nx) * 3 + 2]
self.buffer[(oy * self.width + ox) * 3 + 3] = self.buffer[(ny * self.width + nx) * 3 + 3]
self.buffer[(ny * self.width + nx) * 3 + 1] = tb
self.buffer[(ny * self.width + nx) * 3 + 2] = tt
self.buffer[(ny * self.width + nx) * 3 + 3] = tc
end
end
end
if vertical then
for j = 0, math.ceil(self.cheight / 2) - 1 do
for i = 0, self.cwidth - 1 do
ox, oy, nx, ny = i + self.cx, j + self.cy, i + self.cx, self.cy + self.cheight - j - 1
tb = self.buffer[(oy * self.width + ox) * 3 + 1]
tt = self.buffer[(oy * self.width + ox) * 3 + 2]
tc = self.buffer[(oy * self.width + ox) * 3 + 3]
self.buffer[(oy * self.width + ox) * 3 + 1] = self.buffer[(ny * self.width + nx) * 3 + 1]
self.buffer[(oy * self.width + ox) * 3 + 2] = self.buffer[(ny * self.width + nx) * 3 + 2]
self.buffer[(oy * self.width + ox) * 3 + 3] = self.buffer[(ny * self.width + nx) * 3 + 3]
self.buffer[(ny * self.width + nx) * 3 + 1] = tb
self.buffer[(ny * self.width + nx) * 3 + 2] = tt
self.buffer[(ny * self.width + nx) * 3 + 3] = tc
end
end
end
end
function surf:shift(x, y, b, t, c)
local hdir, vdir = x < 0, y < 0
local xstart, xend = self.cx, self.cx + self.cwidth - 1
local ystart, yend = self.cy, self.cy + self.cheight - 1
local nx, ny
for j = vdir and ystart or yend, vdir and yend or ystart, vdir and 1 or -1 do
for i = hdir and xstart or xend, hdir and xend or xstart, hdir and 1 or -1 do
nx, ny = i - x, j - y
if nx >= 0 and nx < self.width and ny >= 0 and ny < self.height then
self.buffer[(j * self.width + i) * 3 + 1] = self.buffer[(ny * self.width + nx) * 3 + 1]
self.buffer[(j * self.width + i) * 3 + 2] = self.buffer[(ny * self.width + nx) * 3 + 2]
self.buffer[(j * self.width + i) * 3 + 3] = self.buffer[(ny * self.width + nx) * 3 + 3]
else
self.buffer[(j * self.width + i) * 3 + 1] = b
self.buffer[(j * self.width + i) * 3 + 2] = t
self.buffer[(j * self.width + i) * 3 + 3] = c
end
end
end
end
function surf:map(colors)
local c
for j = self.cy, self.cy + self.cheight - 1 do
for i = self.cx, self.cx + self.cwidth - 1 do
c = colors[self.buffer[(j * self.width + i) * 3 + 1]]
if c or self.overwrite then
self.buffer[(j * self.width + i) * 3 + 1] = c
end
end
end
end
surface.palette = { }
surface.palette.cc = {[1]="F0F0F0",[2]="F2B233",[4]="E57FD8",[8]="99B2F2",[16]="DEDE6C",[32]="7FCC19",[64]="F2B2CC",[128]="4C4C4C",[256]="999999",[512]="4C99B2",[1024]="B266E5",[2048]="3366CC",[4096]="7F664C",[8192]="57A64E",[16384]="CC4C4C",[32768]="191919"}
surface.palette.riko4 = {"181818","1D2B52","7E2553","008651","AB5136","5F564F","7D7F82","FF004C","FFA300","FFF023","00E755","29ADFF","82769C","FF77A9","FECCA9","ECECEC"}
local function setPalette(palette)
if palette == _palette then return end
_palette = palette
_rgbpal, _palr, _palg, _palb = { }, { }, { }, { }
local indices = { }
for k, v in pairs(_palette) do
if type(v) == "string" then
_palr[k] = tonumber(v:sub(1, 2), 16) / 255
_palg[k] = tonumber(v:sub(3, 4), 16) / 255
_palb[k] = tonumber(v:sub(5, 6), 16) / 255
elseif type(v) == "number" then
_palr[k] = math.floor(v / 65536) / 255
_palg[k] = (math.floor(v / 256) % 256) / 255
_palb[k] = (v % 256) / 255
end
indices[#indices + 1] = k
end
local pr, pg, pb, dist, d, id
for i = 0, _steps - 1 do
for j = 0, _steps - 1 do
for k = 0, _steps - 1 do
pr = (i + 0.5) / _steps
pg = (j + 0.5) / _steps
pb = (k + 0.5) / _steps
dist = 1e10
for l = 1, #indices do
d = (pr - _palr[indices[l]]) ^ 2 + (pg - _palg[indices[l]]) ^ 2 + (pb - _palb[indices[l]]) ^ 2
if d < dist then
dist = d
id = l
end
end
_rgbpal[i * _steps * _steps + j * _steps + k + 1] = indices[id]
end
end
end
end
function surf:toRGB(palette)
setPalette(palette)
local c
for j = 0, self.height - 1 do
for i = 0, self.width - 1 do
c = self.buffer[(j * self.width + i) * 3 + 1]
self.buffer[(j * self.width + i) * 3 + 1] = _palr[c]
self.buffer[(j * self.width + i) * 3 + 2] = _palg[c]
self.buffer[(j * self.width + i) * 3 + 3] = _palb[c]
end
end
end
function surf:toPalette(palette, dither)
setPalette(palette)
local scale, r, g, b, nr, ng, nb, c, dr, dg, db = _steps - 1
for j = 0, self.height - 1 do
for i = 0, self.width - 1 do
r = self.buffer[(j * self.width + i) * 3 + 1]
g = self.buffer[(j * self.width + i) * 3 + 2]
b = self.buffer[(j * self.width + i) * 3 + 3]
r = (r > 1) and 1 or r
r = (r < 0) and 0 or r
g = (g > 1) and 1 or g
g = (g < 0) and 0 or g
b = (b > 1) and 1 or b
b = (b < 0) and 0 or b
nr = (r == 1) and scale or math_floor(r * _steps)
ng = (g == 1) and scale or math_floor(g * _steps)
nb = (b == 1) and scale or math_floor(b * _steps)
c = _rgbpal[nr * _steps * _steps + ng * _steps + nb + 1]
if dither then
dr = (r - _palr[c]) / 16
dg = (g - _palg[c]) / 16
db = (b - _palb[c]) / 16
if i < self.width - 1 then
self.buffer[(j * self.width + i + 1) * 3 + 1] = self.buffer[(j * self.width + i + 1) * 3 + 1] + dr * 7
self.buffer[(j * self.width + i + 1) * 3 + 2] = self.buffer[(j * self.width + i + 1) * 3 + 2] + dg * 7
self.buffer[(j * self.width + i + 1) * 3 + 3] = self.buffer[(j * self.width + i + 1) * 3 + 3] + db * 7
end
if j < self.height - 1 then
if i > 0 then
self.buffer[((j + 1) * self.width + i - 1) * 3 + 1] = self.buffer[((j + 1) * self.width + i - 1) * 3 + 1] + dr * 3
self.buffer[((j + 1) * self.width + i - 1) * 3 + 2] = self.buffer[((j + 1) * self.width + i - 1) * 3 + 2] + dg * 3
self.buffer[((j + 1) * self.width + i - 1) * 3 + 3] = self.buffer[((j + 1) * self.width + i - 1) * 3 + 3] + db * 3
end
self.buffer[((j + 1) * self.width + i) * 3 + 1] = self.buffer[((j + 1) * self.width + i) * 3 + 1] + dr * 5
self.buffer[((j + 1) * self.width + i) * 3 + 2] = self.buffer[((j + 1) * self.width + i) * 3 + 2] + dg * 5
self.buffer[((j + 1) * self.width + i) * 3 + 3] = self.buffer[((j + 1) * self.width + i) * 3 + 3] + db * 5
if i < self.width - 1 then
self.buffer[((j + 1) * self.width + i + 1) * 3 + 1] = self.buffer[((j + 1) * self.width + i + 1) * 3 + 1] + dr * 1
self.buffer[((j + 1) * self.width + i + 1) * 3 + 2] = self.buffer[((j + 1) * self.width + i + 1) * 3 + 2] + dg * 1
self.buffer[((j + 1) * self.width + i + 1) * 3 + 3] = self.buffer[((j + 1) * self.width + i + 1) * 3 + 3] + db * 1
end
end
end
self.buffer[(j * self.width + i) * 3 + 1] = c
self.buffer[(j * self.width + i) * 3 + 2] = nil
self.buffer[(j * self.width + i) * 3 + 3] = nil
end
end
end
function surface.loadFont(surf)
local font = {width = surf.width, height = surf.height - 1}
font.buffer = { }
font.indices = {0}
font.widths = { }
local startc, hitc, curc = surf.buffer[((surf.height - 1) * surf.width) * 3 + 1]
for i = 0, surf.width - 1 do
curc = surf.buffer[((surf.height - 1) * surf.width + i) * 3 + 1]
if curc ~= startc then
hitc = curc
break
end
end
for j = 0, surf.height - 2 do
for i = 0, surf.width - 1 do
font.buffer[j * font.width + i + 1] = surf.buffer[(j * surf.width + i) * 3 + 1] == hitc
end
end
local curchar = 1
for i = 0, surf.width - 1 do
if surf.buffer[((surf.height - 1) * surf.width + i) * 3 + 1] == hitc then
font.widths[curchar] = i - font.indices[curchar]
curchar = curchar + 1
font.indices[curchar] = i + 1
end
end
font.widths[curchar] = font.width - font.indices[curchar] + 1
return font
end
function surface.getTextSize(str, font)
local cx, cy, maxx = 0, 0, 0
local ox, char = cx
for i = 1, #str do
char = str:byte(i) - 31
if char + 31 == 10 then -- newline
cx = ox
cy = cy + font.height + 1
elseif font.indices[char] then
cx = cx + font.widths[char] + 1
else
cx = cx + font.widths[1]
end
if cx > maxx then
maxx = cx
end
end
return maxx - 1, cy + font.height
end
function surf:drawText(str, x, y, font, b, t, c)
local cx, cy = x + self.ox, y + self.oy
local ox, char, idx = cx
for i = 1, #str do
char = str:byte(i) - 31
if char + 31 == 10 then -- newline
cx = ox
cy = cy + font.height + 1
elseif font.indices[char] then
for i = 0, font.widths[char] - 1 do
for j = 0, font.height - 1 do
x, y = cx + i, cy + j
if font.buffer[j * font.width + i + font.indices[char] + 1] then
if x >= self.cx and x < self.cx + self.cwidth and y >= self.cy and y < self.cy + self.cheight then
idx = (y * self.width + x) * 3
if b or self.overwrite then
self.buffer[idx + 1] = b
end
if t or self.overwrite then
self.buffer[idx + 2] = t
end
if c or self.overwrite then
self.buffer[idx + 3] = c
end
end
end
end
end
cx = cx + font.widths[char] + 1
else
cx = cx + font.widths[1]
end
end
end
end
local sin, cos = math.sin, math.cos
local halfpi = math.pi / 2
Easing = class.new_enum "Easing" {
linear = function( u, d, t )
return u + d * t
end;
smooth = function( u, d, t )
return u + d * ( 3 * t * t - 2 * t * t * t )
end;
exit = function( u, d, t )
return -d * cos(t * halfpi) + d + u
end;
entrance = function( u, d, t )
return u + d * sin(t * halfpi)
end;
-- TODO: probably should add in all the default ones but why are they required?
}
local thrown
local function handler( t )
for i = 1, #t do
if t[i].catch == thrown.name or t[i].default or t[i].catch == thrown.class then
return t[i].handler( thrown )
end
end
return Exception.throw( thrown )
end
Exception = class.new( "Exception", nil, nil ) {
name = "undefined";
data = "undefined";
trace = {};
}
function Exception:Exception( name, data, level )
self.name = name
self.data = data
self.trace = {}
level = ( level or 1 ) + 2
if level > 2 then
for i = 1, 5 do
local src = select( 2, pcall( error, "", level + i ) ):gsub( ": $", "" )
if src == "pcall" or src == "" then
break
else
self.trace[i] = src
end
end
end
end
function Exception:get_traceback( initial, delimiter )
initial = initial or ""
delimiter = delimiter or "\n"
parameters.check( 2, "initial", "string", initial, "delimiter", "string", delimiter )
if #self.trace == 0 then return "" end
return initial .. table.concat( self.trace, delimiter )
end
function Exception:get_data()
if type( self.data ) == "string" or class.is_class( self.data ) or class.is_instance( self.data ) then
return tostring( self.data )
else
return textutils.serialize( self.data )
end
end
function Exception:get_data_and_traceback( indent )
parameters.check( 1, "indent", "number", indent or 1 )
return self:get_data() .. self:get_traceback( "\n" .. (" "):rep( indent or 1 ) .. "in ", "\n" .. (" "):rep( indent or 1 ) .. "in " )
end
function Exception:tostring()
return tostring( self.name ) .. " exception:\n " .. self:get_data_and_traceback( 4 )
end
function Exception.thrown()
return thrown
end
function Exception.throw( e, data, level )
if class.is_class( e ) then
e = e( data, ( level or 1 ) + 1 )
elseif type( e ) == "string" then
e = Exception( e, data, ( level or 1 ) + 1 )
elseif not class.type_of( e, Exception ) then
return Exception.throw( "IncorrectParameterException", "expected class, string, or Exception e, got " .. class.type( e ) )
end
thrown = e
error( "SHEETS_EXCEPTION\nPut code in a try block to catch the exception.", 0 )
end
function Exception.try( func )
local ok, err = pcall( func )
if not ok and err == "SHEETS_EXCEPTION\nPut code in a try block to catch the exception." then
return handler
end
return error( err, 0 )
end
function Exception.catch( etype )
return function( handler )
return { catch = etype, handler = handler }
end
end
function Exception.default( handler )
return { default = true, handler = handler }
end
IncorrectParameterException = class.new( "IncorrectParameterException", Exception, nil ) {
}
function IncorrectParameterException:IncorrectParameterException( data, level )
return self:Exception( "IncorrectParameterException", data, level )
end
IncorrectConstructorException = class.new( "IncorrectConstructorException", Exception, nil ) {
}
function IncorrectConstructorException:IncorrectConstructorException( data, level )
return self:Exception( "IncorrectConstructorException", data, level )
end
ResourceLoadException = class.new( "ResourceLoadException", Exception, nil ) {
}
function ResourceLoadException:ResourceLoadException( data, level )
return self:Exception( "ResourceLoadException", data, level )
end
ThreadRuntimeException = class.new( "ThreadRuntimeException", Exception, nil ) {
}
function ThreadRuntimeException:ThreadRuntimeException( data, level )
return self:Exception( "ThreadRuntimeException", data, level )
end
ICollatedChildren = class.new_interface( "ICollatedChildren", nil ) {
collated_children = {}
}
function ICollatedChildren:ICollatedChildren()
self.collated_children = {}
end
function ICollatedChildren:update_collated( mode, child, data )
local collated = self.collated_children
if mode == "child-added" then
if data == self then
if child:implements( ICollatedChildren ) then
for i = 1, #child.collated_children do
collated[#collated + 1] = child.collated_children[i]
end
end
collated[#collated + 1] = child
else
for i = #collated, 1, -1 do
if collated[i] == data then
if child:implements( ICollatedChildren ) then
i = i - 1 -- so that i + n starts with just i
for n = 1, #child.collated_children do
table.insert( collated, i + n, child.collated_children[n] )
end
table.insert( collated, i + #child.collated_children + 1, child )
else
table.insert( collated, i, child )
end
end
end
end
if self.parent then
self.parent:update_collated( "child-added", child, data )
end
elseif mode == "child-removed" then
local open, close = child:implements( ICollatedChildren ) and child.collated_children[1] or child, child
local removing = false
for i = #collated, 1, -1 do
if collated[i] == close then removing = true end
local brk = collated[i] == open
if removing then table.remove( collated, i ) end
if brk then break end
end
if self.parent then
self.parent:update_collated( "child-removed", child )
end
end
if self.query_tracker then
self.query_tracker:update( mode, child )
end
end
IColoured = class.new_interface( "IColoured", nil ) {
colour = nil;
}
function IColoured:IColoured()
self.values:add( "colour", 1 )
end
local setf, addtag, remtag, query_raw
IQueryable = class.new_interface( "IQueryable", ICollatedChildren ) {
query_tracker = nil;
}
function IQueryable:IQueryable()
self.query_tracker = QueryTracker( self )
end
function IQueryable:iquery( query )
local results = query_raw( self, query, nil, false, false )
local i = 0
return function()
i = i + 1
return results[i], i
end
end
function IQueryable:query( query )
return query_raw( self, query, nil, false, false )
end
function IQueryable:query_tracked( query )
return query_raw( self, query, nil, true, false )
end
function IQueryable:preparsed_query( query, lifetime )
return query_raw( self, query, lifetime, false, true )
end
function IQueryable:preparsed_query_tracked( query, lifetime )
return query_raw( self, query, lifetime, true, true )
end
function setf( self, properties )
local prop_setters = {}
for k, v in pairs( properties ) do
prop_setters[#prop_setters + 1] = { k, "set_" .. k, v }
end
for i = 1, #self do
local vals = self[i].values
for n = 1, #prop_setters do
if vals:has( prop_setters[n][1] ) then
self[i][prop_setters[n][2]]( self[i], prop_setters[n][3] )
end
end
end
end
function addtag( self, tag )
for i = 1, #self do
self[i]:add_tag( tag )
end
end
function remtag( self, tag )
for i = 1, #self do
self[i]:remove_tag( tag )
end
end
function query_raw( self, query, lifetime, track, parsed )
if not parsed then
lifetime = {}
parameters.check( 1, "query", "string", query )
local parser = DynamicValueParser( Stream( query ) )
parser.enable_queries = true
query = parser:parse_query()
end
local query_f, init_f
local nodes = self.collated_children
local matches = { set = setf, add_tag = addtag, remove_tag = remtag }
local n, ID = 0
local function updater() -- this can definitely be optimised
local n = 1
for i = 1, #nodes do
if query_f( nodes[i] ) then
if matches[n] ~= nodes[i] then
table.insert( matches, n, nodes[i] )
self.query_tracker:invoke_child_change( ID, nodes[i], "child-added" )
end
n = n + 1
elseif matches[n] == nodes[i] then
table.remove( matches, n )
self.query_tracker:invoke_child_change( ID, nodes[i], "child-removed" )
end
end
end
query_f, init_f = Codegen.node_query( query, lifetime, updater )
init_f( self )
for i = 1, #nodes do
if query_f( nodes[i] ) then
n = n + 1
matches[n] = nodes[i]
end
end
if track then
ID = self.query_tracker:track( query_f, matches )
self.query_tracker.lifetimes[ID] = lifetime
return matches, ID
else
if not parsed then
for i = #lifetime, 1, -1 do
local l = lifetime[i]
lifetime[i] = nil
if l[1] == "value" then
l[2].values:unsubscribe( l[3], l[4] )
elseif l[1] == "query" then
l[2]:unsubscribe( l[3], l[4] )
elseif l[1] == "tag" then
l[2]:unsubscribe_from_tag( l[3], l[4] )
end
end
end
return matches
end
end
IChildContainer = class.new_interface( "IChildContainer", ICollatedChildren, IQueryable ) {
children = {};
application = nil;
}
function IChildContainer:IChildContainer()
self.children = {}
self.meta.__add = self.add_child
function self.meta:__concat( child )
self:add_child( child )
return self
end
end
function IChildContainer:child_value_changed( child )
self.query_tracker:update( "child-changed", child )
if self.parent then
return self.parent:child_value_changed( child )
end
end
function IChildContainer:add_child( child )
parameters.check( 1, "child", Sheet, child )
local children = self.children
local collated = self.collated_children
if child.parent then
child.parent:remove_child( child, true )
end
local index = #children + 1
for i = 1, #children do
if children[i].z > child.z then
index = i
break
end
end
local c, l = children[index], index <= #children
table.insert( children, index, child )
self:update_collated( "child-added", child, l and (c:implements( ICollatedChildren ) and c.collated_children[1] or c) or self )
self:set_changed()
if child:implements( ICollatedChildren ) then
for i = 1, #child.collated_children do
child.collated_children[i].application = self.application
child.collated_children[i].values:trigger "application"
end
end
child.parent = self
child.application = self.application
child.values:trigger "parent"
child.values:trigger "application"
child.values:child_inserted()
return child
end
function IChildContainer:remove_child( child, reinsert )
for i = 1, #self.children do
if self.children[i] == child then
child.parent = nil
child.application = nil
table.remove( self.children, i )
self:set_changed()
self:update_collated( "child-removed", child )
if child:implements( ICollatedChildren ) then
for i = 1, #child.collated_children do
child.collated_children[i].application = nil
child.collated_children[i].values:trigger "application"
end
end
child.values:trigger "parent"
child.values:trigger "application"
if not reinsert then
child.values:child_removed()
end
return child
end
end
end
function IChildContainer:get_children()
local c = {}
local children = self.children
for i = 1, #children do
c[i] = children[i]
end
return c
end
function IChildContainer:get_children_at( x, y )
parameters.check( 2, "x", "number", x, "y", "number", y )
local c = self:get_children()
local elements = {}
for i = #c, 1, -1 do
c[i]:handle( MouseEvent( 6, x - c[i].x, y - c[i].y, elements, true ) )
end
return elements
end
function IChildContainer:is_child_visible( child )
parameters.check( 1, "child", Sheet, child )
return child.x + child.width > 0 and child.y + child.height > 0 and child.x < self.width and child.y < self.height
end
function IChildContainer:reposition_child_z_index( child )
local children = self.children
for i = 1, #children do
if children[i] == child then
local moved = false
while children[i-1] and children[i-1].z > child.z do
children[i-1], children[i] = child, children[i-1]
moved = true
i = i - 1
end
while children[i+1] and children[i+1].z < child.z do
children[i+1], children[i] = child, children[i+1]
moved = true
i = i + 1
end
if moved then
self:update_collated( "child-removed", child )
self:update_collated( "child-added", child, i + 1 > #children and self or children[i + 1]:implements( ICollatedChildren ) and children[i + 1].collated_children[1] or children[i + 1] )
self:set_changed()
end
break
end
end
end
ITagged = class.new_interface( "ITagged", nil ) {
tags = {};
subscriptions = {};
id = "ID";
}
function ITagged:ITagged()
self.tags = {}
self.subscriptions = {}
end
function ITagged:add_tag( tag )
self.tags[tag] = true
if self.parent then
self.parent:child_value_changed( self )
end
return self:trigger( tag )
end
function ITagged:remove_tag( tag )
self.tags[tag] = ni
if self.parent then
self.parent:child_value_changed( self )
end
return self:trigger( tag )
end
function ITagged:has_tag( tag )
return self.tags[tag] or false
end
function ITagged:toggle_tag( tag )
self.tags[tag] = not self.tags[tag] or nil
if self.parent then
self.parent:child_value_changed( self )
end
return self:trigger( tag )
end
function ITagged:set_ID( id ) -- TODO: make this a dynamic property
self.id = tostring( id )
if self.parent then
self.parent:child_value_changed( self )
end
return self
end
function ITagged:subscribe_to_tag( tag, lifetime, callback )
self.subscriptions[tag] = self.subscriptions[tag] or {}
self.subscriptions[tag][#self.subscriptions[tag] + 1] = callback
lifetime[#lifetime + 1] = { "tag", self, tag, callback }
return callback
end
function ITagged:unsubscribe_from_tag( tag, f )
if self.subscriptions[tag] then
for i = #self.subscriptions[tag], 1, -1 do
if self.subscriptions[tag][i] == f then
return table.remove( self.subscriptions[tag], i )
end
end
end
end
function ITagged:trigger( tag )
if self.subscriptions[tag] then
for i = #self.subscriptions[tag], 1, -1 do
self.subscriptions[tag][i]()
end
end
return self
end
ISize = class.new_interface( "ISize", nil ) {
width = 0;
height = 0;
}
function ISize:ISize()
self.values:add( "width", 0, { update_surface_size = true } )
self.values:add( "height", 0, { update_surface_size = true } )
end
ITimer = class.new_interface( "ITimer", nil ) {
timerID = 0;
time = nil;
lt = nil;
timers = {};
}
function ITimer:ITimer()
self.time = os.clock()
self.timers = {}
self:step_timer()
end
function ITimer:new_timer( n )
parameters.check( 1, "n", "number", n )
local finish, ID = self.time + n, nil -- avoids duplicating timer events
for i = 1, #self.timers do
if self.timers[i].time == finish then
ID = self.timers[i].ID
break
end
end
return ID or os.startTimer( n )
end
function ITimer:queue( response, n )
parameters.check( 2, "response", "function", response, "n", "number", n )
local timer_id = self:new_timer( n )
local finish = self.time + n
self.timers[#self.timers + 1] = { time = finish, response = response, ID = timer_id }
return timer_id
end
function ITimer:cancel_timer( ID )
parameters.check( 1, "ID", "number", ID )
for i = #self.timers, 1, -1 do
if self.timers[i].ID == ID then
table.remove( self.timers, i )
break
end
end
return self
end
function ITimer:step_timer()
self.lt = self.time
self.time = os.clock()
return self
end
function ITimer:get_timer_delta()
return self.time - self.lt
end
function ITimer:update_timer( timer_id )
local updated = false
for i = #self.timers, 1, -1 do
if self.timers[i].ID == timer_id then
table.remove( self.timers, i ).response()
updated = true
end
end
return updated
end
Event = class.new( "Event", nil, nil ) {
event = "Event";
}
function Event:is( event )
return self.event == event
end
function Event:handle( handler )
self.handled = true
self.handler = handler
end
KeyboardEvent = class.new( "KeyboardEvent", Event, nil ) {
event = "KeyboardEvent";
key = 0;
held = {};
}
function KeyboardEvent:KeyboardEvent( event, key, held )
self.event = event
self.key = key
self.held = held
end
function KeyboardEvent:matches( hotkey )
local t, segment2
for segment in hotkey:gmatch "(.-)%-" do
if segment == "ctrl" or segment == "shift" or segment == "alt" then
segment = segment:sub( 1, 1 ):upper() .. segment:sub( 2 )
segment2 = "right" .. segment
segment = "left" .. segment
if self.held[segment2] then
if self.held[segment] then
segment = self.held[segment] < self.held[segment2] and (not t or self.held[segment] > t) and segment or segment2
else
segment = segment2
end
end
end
if not self.held[segment] or ( t and self.held[segment] < t ) then
return false
end
t = self.held[segment]
end
return self.key == keys[hotkey:gsub( ".+%-", "" )]
end
function KeyboardEvent:is_held( key )
return self.key == keys[key] or self.held[key]
end
MiscEvent = class.new( "MiscEvent", Event, nil ) {
event = "MiscEvent";
parameters = {};
}
function MiscEvent:MiscEvent( event, ... )
self.event = event
self.parameters = { ... }
end
MouseEvent = class.new( "MouseEvent", Event, nil ) {
event = "MouseEvent";
x = 0;
y = 0;
button = 0;
within = true;
}
function MouseEvent:MouseEvent( event, x, y, button, within )
self.event = event
self.x = x
self.y = y
self.button = button
self.within = within
end
function MouseEvent:is_within_area( x, y, width, height )
parameters.check( 4,
"x", "number", x,
"y", "number", y,
"width", "number", width,
"height", "number", height
)
return self.x >= x and self.y >= y and self.x < x + width and self.y < y + height
end
function MouseEvent:clone( x, y, within )
parameters.check( 2,
"x", "number", x,
"y", "number", y
)
local sub = MouseEvent( self.event, self.x - x, self.y - y, self.button, self.within and within or false )
sub.handled = self.handled
function sub.handle()
sub.handled = true
self:handle()
end
return sub
end
TextEvent = class.new( "TextEvent", Event, nil ) {
event = "TextEvent";
text = "";
}
function TextEvent:TextEvent( event, text )
self.event = event
self.text = text
end
local property_cache = {}
local CHANGECODE_NO_TRANSITION, CHANGECODE_TRANSITION, SELF_INDEX_UPDATER,
ARBITRARY_DOTINDEX_UPDATER, ARBITRARY_INDEX_UPDATER, DYNAMIC_QUERY_UPDATER,
QUERY_UPDATER, GENERIC_SETTER, STRING_CASTING, RAW_STRING_CASTING,
INTEGER_CASTING, RAW_INTEGER_CASTING, NUMBER_CASTING, RAW_NUMBER_CASTING,
COLOUR_CASTING, RAW_COLOUR_CASTING, ALIGNMENT_CASTING,
RAW_ALIGNMENT_CASTING, ERR_CASTING
local node_query_internal, dynamic_value_internal
Codegen = class.new( "Codegen", nil, nil ) {
}
function Codegen.node_query( parsed_query, lifetime, updater )
local names = {}
local named_values = {}
local val_names = {}
local init_localised = {}
local initialise_code = {}
local tracked = {}
local query_str = node_query_internal( parsed_query, "n", tracked )
local tl = #tracked
for i = 1, tl do
names[i] = "n" .. i
named_values[i] = tracked[i].value
init_localised[i] = "f" .. i
init_localised[tl + i] = "i" .. i
val_names[i] = "v" .. i
initialise_code[i] = "f" .. i .. ", i" .. i .. " = Codegen.dynamic_value( n" .. i .. ", lifetime, env, n, function()\n"
.. "\tv" .. i .. " = f" .. i .. "()\n"
.. "\treturn updater()\n"
.. "end )"
end
for i = 1, tl do
initialise_code[i + tl] = "i" .. i .. "()"
end
for i = 1, tl do
initialise_code[i + tl + tl] = "v" .. i .. " = f" .. i .. "()"
end
local code = "local lifetime, updater" .. (#names == 0 and "" or ", " .. table.concat( names, ", " )) .. " = ...\n"
.. (#val_names == 0 and "" or "local " .. table.concat( val_names, ", " ) .. "\n")
.. "return function( n )\n"
.. "\treturn " .. query_str
.. "\nend, function( n )\n"
.. "\tlocal env = {}\n"
.. (#init_localised == 0 and "" or "\tlocal " .. table.concat( init_localised, ", " ) .. "\n")
.. table.concat( initialise_code, "\n" )
.. "\nend"
local f, err = assert( (load or loadstring)( code, "query", nil, _ENV ) )
if setfenv then
setfenv( f, getfenv() )
end
local getter, initialiser = f( lifetime, updater, unpack( named_values ) )
return getter, initialiser
end
function Codegen.dynamic_value( parsed_value, lifetime, env, obj, updater )
local names = {}
local functions = {}
local inputs = {}
local state = {
environment = env;
object = obj;
names = names;
functions = functions;
inputs = inputs;
}
local return_value = dynamic_value_internal( parsed_value, state )
local roots = {}
local roots_tocheck = { return_value }
local i = 1
local func_compiled = {}
local initialisers = {}
local initialise_function
local input_names = {}
for i = 1, #inputs do
input_names[i] = "i" .. i
end
while i <= #roots_tocheck do
local t = roots_tocheck[i]
if #t.dependencies == 0 then
roots[#roots + 1] = t
else
local added = false
for n = 1, #t.dependencies do
if t.dependencies[n].update or t.dependencies[n].initialise then
roots_tocheck[#roots_tocheck + 1] = t.dependencies[n]
added = true
end
end
if not added then
roots[#roots + 1] = t
end
end
i = i + 1
end
for i = 1, #functions do
local dependants = {}
local tocheck = { functions[i].node }
local index = 1
local update_root = false
while index <= #tocheck do
if index ~= 1 then
dependants[#dependants + 1] = tocheck[index].update
end
if index == 1 or not tocheck[index].complex then
update_root = update_root or tocheck[index] == return_value
local idx = #tocheck
for n = 1, #tocheck[index].dependants do
tocheck[idx + n] = tocheck[index].dependants[n]
end
end
index = index + 1
end
if update_root then
dependants[#dependants + 1] = "updater()"
end
if dependants[1] then
dependants[#dependants] = "return " .. dependants[#dependants]
end
func_compiled[i] = functions[i].code:gsub( "DEPENDENCIES", table.concat( dependants, "\n" ) )
end
local i = 1
while i <= #roots do
initialisers[#initialisers + 1] = roots[i].initialise or roots[i].update
if not roots[i].complex then
for n = 1, #roots[i].dependants do
roots[#roots + 1] = roots[i].dependants[n]
end
end
i = i + 1
end
local s = initialisers[#initialisers]
if s and s:find "^f%d+%(%)" then
if #initialisers == 1 then
initialise_function = s:match "^f%d+"
else
initialisers[#initialisers] = "return " .. s
end
end
local code
= "local self, lifetime, updater"
.. (#inputs > 0 and ", " .. table.concat( input_names, ", ") or "")
.. " = ...\n"
.. (state.tostringed and "local tostring = tostring\n" or "")
.. (state.floored and "local floor = math.floor\n" or "")
.. (#names > 0 and "local " .. table.concat( names, ", " ) .. "\n" or "")
.. table.concat( func_compiled, "\n" ) .. "\n"
.. "return function() return " .. return_value.value .. " end, "
.. (initialise_function or "function()\n"
.. table.concat( initialisers, "\n" )
.. (#initialisers == 0 and "" or "\n") .. "end")
if parsed_value.type == "binary operator expression" and parsed_value.lvalue.lvalue and parsed_value.lvalue.lvalue.type == "tag" then
local h = fs.open( "demo/log.txt", "w" )
h.write( code )
h.close()
end
local f, err = assert( (load or loadstring)( code, "dynamic value", nil, _ENV ) )
if setfenv then
setfenv( f, getfenv() )
end
local getter, initialiser = f( obj, lifetime, updater, unpack( inputs ) )
return getter, initialiser
end
function Codegen.dynamic_property_setter( property, options )
property_cache[property] = property_cache[property] or {}
options = options or {}
local self_changed = ValueHandler.properties[property].change == "self"
local parent_changed = ValueHandler.properties[property].change == "parent"
local ptype = ValueHandler.properties[property].type
local t1 = {}
local t2 = {}
local t3 = {}
local t4 = {}
local t5 = {}
if options.update_surface_size then
t4[#t4 + 1] = "if self.surface then self.surface = surface.create( self.width, self.height ) end"
self_changed = true
end
if self_changed then
t4[#t4 + 1] = "if not self.changed then self:set_changed() end"
elseif parent_changed then
t4[#t4 + 1] = "if self.parent then self.parent:set_changed() end"
end
if self_changed or parent_changed then
t4[#t4 + 1] = "if self.parent then self.parent:child_value_changed( self ) end"
end
if ptype == Type.primitive.string then
t1[#t1 + 1] = "if value:sub( 1, 1 ) == '!' then value = value:sub( 2 ) else value = ('%q'):format( value ) end"
end
if ptype == Type.sheets.colour then
for k, v in pairs( colour ) do
t2[#t2 + 1] = "environment." .. k .. " = { type = rtype, value = " .. v .. " }"
end
t5[#t5 + 1] = "if value == 0 then value = nil end"
end
if ptype == Type.sheets.alignment then
for k, v in pairs( alignment ) do
t2[#t2 + 1] = "environment." .. k .. " = { type = rtype, value = " .. v .. " }"
end
end
t4[#t4 + 1] = options.custom_update_code
local s5 = table.concat( t5, "\n" ) -- code to update the value before assignment
local s4 = table.concat( t4, "\n" ) -- code to run on value update
local s3 = table.concat( t3, "\n" ) -- code to update the AST
local s2 = table.concat( t2, "\n" ) -- code to change the environment
local s1 = table.concat( t1, "\n" ) -- code to update the string value
for i = 1, #property_cache[property] do
local c = property_cache[property][i]
if c[1] == s1 and c[2] == s2 and c[3] == s3 and c[4] == s4 and c[5] == s5 then
return c.f
end
end
local change_code
if ValueHandler.properties[property].transitionable then
change_code = CHANGECODE_TRANSITION
if s4 ~= "" then
change_code = change_code
:gsub( "CUSTOM_UPDATE", ", function( self )\n" .. s4 .. "\nend" )
:gsub( "PROPERTY_TRANSITION_QUOTED", ("%q"):format( property .. "_transition" ) )
:gsub( "PROCESS_VALUE", s5 )
end
else
change_code = CHANGECODE_NO_TRANSITION
:gsub( "ONCHANGE", s4 )
:gsub( "PROCESS_VALUE", s5 )
end
local prop_quoted = ("%q"):format( property )
local caster = ptype == Type.primitive.string and STRING_CASTING
or ptype == Type.primitive.integer and INTEGER_CASTING
or ptype == Type.primitive.number and NUMBER_CASTING
or ptype == Type.sheets.colour and COLOUR_CASTING
or ptype == Type.sheets.alignment and ALIGNMENT_CASTING
or ERR_CASTING
local rawcaster = ptype == Type.primitive.string and RAW_STRING_CASTING
or ptype == Type.primitive.integer and RAW_INTEGER_CASTING
or ptype == Type.primitive.number and RAW_NUMBER_CASTING
or ptype == Type.sheets.colour and RAW_COLOUR_CASTING
or ptype == Type.sheets.alignment and RAW_ALIGNMENT_CASTING
or ERR_CASTING
local str = GENERIC_SETTER
:gsub( "CHANGECODE", change_code )
:gsub( "PROPERTY_QUOTED", ("%q"):format( property ) )
:gsub( "RAW_PROPERTY", ("%q"):format( "raw_" .. property ) )
:gsub( "VALUE_MODIFICATION", function() return s1 end )
:gsub( "ENV_MODIFICATION", function() return s2 end )
:gsub( "AST_MODIFICATION", function() return s3 end )
:gsub( "CASTING_RAW", function() return rawcaster end )
:gsub( "CASTING", function() return caster end )
local f = assert( (load or loadstring)( str, "property setter '" .. property .. "'", nil, _ENV ) )
if setfenv then
setfenv( f, getfenv() )
end
local fr = f( ptype )
property_cache[property][#property_cache[property] + 1] = { s1, s2, s3, s4, s5, f = fr }
return fr
end
CHANGECODE_NO_TRANSITION = [[
PROCESS_VALUE
self[PROPERTY_QUOTED] = value
ONCHANGE
self.values:trigger PROPERTY_QUOTED]]
CHANGECODE_TRANSITION = [[
PROCESS_VALUE
self.values:transition( PROPERTY_QUOTED, value, self[PROPERTY_TRANSITION_QUOTED]CUSTOM_UPDATE )]]
STRING_CASTING = [[
if value_type == Type.primitive.integer or value_type == Type.primitive.number or value_type == Type.primitive.boolean then
value_parsed = {
type = "tostring";
value = value_parsed;
}
else
error "TODO: fix this error"
end
]]
RAW_STRING_CASTING = [[
if value_type == Type.primitive.integer or value_type == Type.primitive.number or value_type == Type.primitive.boolean then
value = tostring( value )
else
error "TODO: fix this error"
end
]]
INTEGER_CASTING = [[
if value_type == Type.primitive.number then
value_parsed = {
type = "floor";
value = value_parsed;
}
else
error "TODO: fix this error"
end
]]
RAW_INTEGER_CASTING = [[
if value_type == Type.primitive.number then
value = math.floor( value )
else
error "TODO: fix this error"
end
]]
NUMBER_CASTING = [[
if not (value_type == Type.primitive.integer) then
error "TODO: fix this error"
end
]]
RAW_NUMBER_CASTING = NUMBER_CASTING
COLOUR_CASTING = [[
error "TODO: fix this error"
]]
RAW_COLOUR_CASTING = [[
if value_type == Type.primitive.integer then
if value ~= 0 and (math.log( value ) / math.log( 2 ) % 1 ~= 0 or value < 1 or value > 2 ^ 15) then
error "TODO: fix this error"
end
else
error "TODO: fix this error"
end
]]
ALIGNMENT_CASTING = [[
error "TODO: fix this error"
]]
RAW_ALIGNMENT_CASTING = [[
if value_type == Type.primitive.integer then
if value ~= 0 and value ~= 2 and value ~= 3 and value ~= 4 and value ~= 1 then
error "TODO: fix this error"
end
else
error "TODO: fix this error"
end
]]
ERR_CASTING = [[
error "TODO: fix this error"
]]
SELF_INDEX_UPDATER = [[function FUNC()
NAME = self.INDEX
DEPENDENCIES
end]]
ARBITRARY_DOTINDEX_UPDATER = [[do
local function f0()
DEPENDENCIES
end
function FUNC()
local obj = LVALUE
if NAME then
NAME.values:unsubscribe( "INDEX", f0 )
end
if obj then
obj.values:subscribe( "INDEX", lifetime, f0 )
end
NAME = obj
return f0()
end
end]]
TAG_CHECK_UPDATER = [[do
local function f0()
VALUE = NAME and NAME:has_tag TAG
DEPENDENCIES
end
function FUNC()
local obj = LVALUE
if NAME then
NAME:unsubscribe_from_tag( TAG, f0 )
end
if obj then
obj:subscribe_to_tag( TAG, lifetime, f0 )
end
NAME = obj
return f0()
end
end]]
ARBITRARY_INDEX_UPDATER = [[do
local function f0()
NAME = OLDVALUE and OLDINDEX and OLDVALUE[OLDINDEX]
DEPENDENCIES
end
function FUNC()
local obj = LVALUE
local idx = INDEX
if OLDVALUE and type( OLDINDEX ) == "string" then
OLDVALUE.values:unsubscribe( OLDINDEX, f0 )
end
if obj and type( idx ) == "string" then
obj.values:subscribe( idx, lifetime, f0 )
end
OLDVALUE = obj
OLDINDEX = idx
return f0()
end
end]]
DYNAMIC_QUERY_UPDATER = [[do
local elems, ID
local function f0()
NAME = elems and elems[1]
DEPENDENCIES
end
function FUNC()
local object = SOURCE
if PREVSOURCE then
PREVSOURCE.query_tracker:unsubscribe( ID, f0 )
end
if object then
elems, ID = object:preparsed_query_tracked( QDATA, lifetime )
object.query_tracker:subscribe( ID, lifetime, f0 )
end
PREVSOURCE = object
return f0()
end
end]]
QUERY_UPDATER = [[function FUNC()
local object = SOURCE
if object then
local elems = object:preparsed_query( QDATA, lifetime )
NAME = elems[1]
if NAME then
FUNC = function()end
DEPENDENCIES
end
end
end]]
GENERIC_SETTER = [[
local rtype = ...
return function( self, value )
self.values:respawn PROPERTY_QUOTED
self[RAW_PROPERTY] = value
if type( value ) ~= "string" then
local value_type = Typechecking.resolve_type( value )
if not (value_type == rtype) then
print( value_type, rtype )
CASTING_RAW
end
CHANGECODE
return self
end
VALUE_MODIFICATION
local parser = DynamicValueParser( Stream( value ) )
local environment = {}
parser.flags.enable_queries = true
ENV_MODIFICATION
local value_parsed = parser:parse_expression()
or "TODO: fix this error"
AST_MODIFICATION
local value_parsed, value_type = Typechecking.check_type( value_parsed, {
object = self;
environment = environment;
} )
local lifetime = self.values.lifetimes[PROPERTY_QUOTED]
local default = self.values .defaults[PROPERTY_QUOTED]
local setter_f, initialiser_f
if not (value_type == rtype) then
CASTING
end
local function update()
local value = setter_f( self ) or default
if value ~= self[PROPERTY_QUOTED] then
CHANGECODE
end
end
if not parser.stream:is_EOF() then
error "TODO: fix this error"
end
setter_f, initialiser_f = Codegen.dynamic_value( value_parsed, lifetime, environment, self, update )
initialiser_f()
update()
return self
end]]
function node_query_internal( query, name, tracked )
if query.type == "id" then
return ("%s.id=='%s'"):format( name, query.value )
elseif query.type == "tag" then
return ("%s:has_tag'%s'"):format( name, query.value )
elseif query.type == "any" then
return "true"
elseif query.type == "class" then
return ("%s:type():lower()=='%s'"):format( name, query.value:lower() )
elseif query.type == "negate" then
local i = node_query_internal( query.value, name, tracked )
return i == "true" and "false" or i == "false" and "true" or "not (" .. i .. ")"
elseif query.type == "attributes" then
local t = {}
local idx = #tracked + 1
for i = 1, #query.attributes do
local attr = query.attributes[i]
local op = attr.comparison
if op == "=" then
op = "=="
end
tracked[idx] = { value = attr.value }
t[i] = "v" .. idx .. " and " .. name .. "." .. attr.name .. " " .. op .. " v" .. idx
idx = idx + 1
end
return table.concat( t, " and " )
elseif query.type == "operator" then
if query.operator == "&" then
local lvalue = node_query_internal( query.lvalue, name, tracked )
local rvalue = node_query_internal( query.rvalue, name, tracked )
if lvalue == "true" then return rvalue end
if rvalue == "true" then return lvalue end
if lvalue == "false" then return lvalue end
if rvalue == "false" then return rvalue end
return lvalue .. " and " .. rvalue
elseif query.operator == "|" then
local lvalue = node_query_internal( query.lvalue, name, tracked )
local rvalue = node_query_internal( query.rvalue, name, tracked )
if lvalue == "true" then return lvalue end
if rvalue == "true" then return rvalue end
if lvalue == "false" then return rvalue end
if rvalue == "false" then return lvalue end
return "(" .. lvalue .. " or " .. rvalue .. ")"
elseif query.operator == ">" then
local lvalue = node_query_internal( query.lvalue, name .. ".parent", tracked )
local rvalue = node_query_internal( query.rvalue, name, tracked )
if lvalue == "true" then return name .. ".parent and " .. rvalue end
if rvalue == "true" then return name .. ".parent and " .. lvalue end
if lvalue == "false" then return lvalue end
if rvalue == "false" then return rvalue end
return rvalue .. " and " .. name .. ".parent and " .. lvalue
end
end
end
function dynamic_value_internal( value, state )
if not value then return error "here" end
if value.type == "integer"
or value.type == "float"
or value.type == "boolean" then
return {
value = tostring( value.value );
complex = false;
update = nil;
initialise = nil;
dependants = {};
dependencies = {};
}
elseif value.type == "string" then
return {
value = ("%q"):format( value.value );
complex = false;
update = nil;
initialise = nil;
dependants = {};
dependencies = {};
}
elseif value.type == "self" then
return {
value = "self";
complex = false;
update = nil;
initialise = nil;
dependants = {};
dependencies = {};
}
elseif value.type == "parent" then
if state.object:type_of( Application ) then
error "TODO: fix this error"
else
local nr = #state.names + 1
local nu = #state.names + 2
local t = {
value = "n" .. nr;
complex = true;
update = "f" .. nu .. "()";
initialise = "self.values:subscribe( 'parent', lifetime, f" .. nu .. " )\nf" .. nu .. "()";
dependants = {};
dependencies = {};
}
state.names[nr] = "n" .. nr
state.names[nu] = "f" .. nu
state.functions[#state.functions + 1] = {
code = SELF_INDEX_UPDATER:gsub( "NAME", "n" .. nr ):gsub( "INDEX", "parent" ):gsub( "FUNC", "f" .. nu );
node = t;
}
return t
end
elseif value.type == "application" then
if state.object:type_of( Application ) then
return {
value = "self";
complex = false;
update = nil;
initialise = nil;
dependants = {};
dependencies = {};
}
else
local nr = #state.names + 1
local nu = #state.names + 2
local t = {
value = "n" .. nr;
complex = true;
update = "f" .. nu .. "()";
initialise = "self.values:subscribe( 'application', lifetime, f" .. nu .. " )\nf" .. nu .. "()";
dependants = {};
dependencies = {};
}
state.names[nr] = "n" .. nr
state.names[nu] = "f" .. nu
state.functions[#state.functions + 1] = {
code = SELF_INDEX_UPDATER:gsub( "NAME", "n" .. nr ):gsub( "INDEX", "application" ):gsub( "FUNC", "f" .. nu );
node = t;
}
return t
end
elseif value.type == "identifier" then
if state.environment[value.value] ~= nil then
state.inputs[#state.inputs + 1] = state.environment[value.value].value;
return {
value = "i" .. #state.inputs;
complex = false;
update = nil;
initialise = nil;
dependants = {};
dependencies = {};
}
else
error "TODO: fix this error"
end
elseif value.type == "percentage" then
error "NYI"
elseif value.type == "unary operator expression" then
local val = dynamic_value_internal( value.value, state )
local n = #state.names + 1
local t = {
value = "n" .. n;
complex = false;
update = "n" .. n .. " = " .. val.value .. " ~= nil and " .. value.operator .. " " .. val.value .. " or nil";
initialise = nil;
dependants = {};
dependencies = { val };
}
state.names[n] = "n" .. n
val.dependants[#val.dependants + 1] = t
return t
elseif value.type == "call" then
local val = dynamic_value_internal( value.value, state )
local params = {}
local params_strval = {}
local n = #state.names + 1
for i = 1, #value.parameters do
params[i] = dynamic_value_internal( value.parameters[i], state )
params_strval[i] = params[i].value
end
local t = {
value = "n" .. n;
complex = false;
update = "n" .. n .. " = " .. val.value .. " ~= nil and " .. table.concat( params_strval, " ~= nil and " ) .. " ~= nil and " .. val.value .. "(" .. table.concat( params_strval, ", " ) .. ") or nil";
initialise = nil;
dependants = {};
dependencies = { val, unpack( params ) };
}
state.names[n] = "n" .. n
val.dependants[#val.dependants + 1] = t
for i = 1, #params do
params[i].dependants[#params[i].dependants + 1] = t
end
return t
elseif value.type == "index" then
local val = dynamic_value_internal( value.value, state )
local idx = dynamic_value_internal( value.index, state )
local nval = #state.names + 1 -- copy of the value
local nidx = #state.names + 2 -- copy of the index
local nret = #state.names + 3 -- return value
local npdt = #state.names + 4 -- updater function
local t = {
value = "n" .. nret;
complex = true;
update = "f" .. npdt .. "()";
initialise = nil;
dependants = {};
dependencies = { val, idx };
}
val.dependants[#val.dependants + 1] = t;
idx.dependants[#idx.dependants + 1] = t;
state.names[nval] = "n" .. nval
state.names[nidx] = "n" .. nidx
state.names[nret] = "n" .. nret
state.names[npdt] = "f" .. npdt
state.functions[#state.functions + 1] = {
code = ARBITRARY_INDEX_UPDATER
:gsub( "NAME", "n" .. nret )
:gsub( "FUNC", "f" .. npdt )
:gsub( "OLDVALUE", "n" .. nval )
:gsub( "OLDINDEX", "n" .. nidx )
:gsub( "LVALUE", val.value )
:gsub( "INDEX", idx.value );
node = t;
}
return t
elseif value.type == "binary operator expression" then
local lvalue = dynamic_value_internal( value.lvalue, state )
local rvalue = dynamic_value_internal( value.rvalue, state )
local n = #state.names + 1
local t = {
value = "n" .. n;
complex = false;
update = "n" .. n .. " = " .. (
(value.operator == "or" and lvalue.value .. " or " .. rvalue.value)
or (value.operator == "and" and lvalue.value .. " and " .. rvalue.value .. " or nil")
-- or value.operator == "==" and "" -- potentially $abc == $def == true if both are undefined
-- or value.operator == "~=" and "" -- potentially $abc != $def == true if one is undefined and false if both are undefined
or (lvalue.value .. " ~= nil and " .. rvalue.value .. " ~= nil and " .. lvalue.value .. " " .. value.operator .. " " .. rvalue.value .. " or nil")
);
initialise = nil;
dependants = {};
dependencies = { lvalue, rvalue };
}
state.names[n] = "n" .. n
lvalue.dependants[#lvalue.dependants + 1] = t
rvalue.dependants[#rvalue.dependants + 1] = t
return t
elseif value.type == "dotindex" then
local val = dynamic_value_internal( value.value, state )
local nr = #state.names + 1
local nu = #state.names + 2
local t = {
value = "(n" .. nr .. " and n" .. nr .. "." .. value.index .. ")";
complex = true;
update = "f" .. nu .. "()";
initialise = nil;
dependants = {};
dependencies = { val };
}
state.names[nr] = "n" .. nr
state.names[nu] = "f" .. nu
state.functions[#state.functions + 1] = {
code = ARBITRARY_DOTINDEX_UPDATER
:gsub( "NAME", "n" .. nr )
:gsub( "INDEX", value.index )
:gsub( "FUNC", "f" .. nu )
:gsub( "LVALUE", val.value );
node = t;
}
val.dependants[#val.dependants + 1] = t
return t
elseif value.type == "query" then
local val = dynamic_value_internal( value.source, state )
local nret = #state.names + 1
local npdt = #state.names + 2
local t = {
value = "n" .. nret;
complex = true;
update = "f" .. npdt .. "()";
initialise = nil;
dependants = {};
dependencies = { val };
}
state.inputs[#state.inputs + 1] = value.query
state.names[nret] = "n" .. nret
state.names[npdt] = "f" .. npdt
state.functions[#state.functions + 1] = {
code = QUERY_UPDATER
:gsub( "NAME", "n" .. nret )
:gsub( "SOURCE", val.value )
:gsub( "QDATA", "i" .. #state.inputs )
:gsub( "FUNC", "f" .. npdt );
node = t;
}
val.dependants[#val.dependants + 1] = t
return t
elseif value.type == "dynamic query" then
local val = dynamic_value_internal( value.source, state )
local nret = #state.names + 1
local nsrc = #state.names + 2
local npdt = #state.names + 3
local t = {
value = "n" .. nret;
complex = true;
update = "f" .. npdt .. "()";
initialise = nil;
dependants = {};
dependencies = { val };
}
state.inputs[#state.inputs + 1] = value.query
state.names[nret] = "n" .. nret
state.names[nsrc] = "n" .. nsrc
state.names[npdt] = "f" .. npdt
state.functions[#state.functions + 1] = {
code = DYNAMIC_QUERY_UPDATER
:gsub( "NAME", "n" .. nret )
:gsub( "PREVSOURCE", "n" .. nsrc )
:gsub( "SOURCE", val.value )
:gsub( "QDATA", "i" .. #state.inputs )
:gsub( "FUNC", "f" .. npdt );
node = t;
}
val.dependants[#val.dependants + 1] = t
return t
elseif value.type == "floor" then
local val = dynamic_value_internal( value.value, state )
local n = #state.names + 1
local t = {
value = "n" .. n;
complex = false;
update = "n" .. n .. " = " .. val.value .. " ~= nil and floor( " .. val.value .. " ) or nil";
initialise = nil;
dependants = {};
dependencies = { val };
}
state.names[n] = "n" .. n
val.dependants[#val.dependants + 1] = t
state.floored = true
return t
elseif value.type == "tostring" then
local val = dynamic_value_internal( value.value, state )
local n = #state.names + 1
local t = {
value = "n" .. n;
complex = false;
update = "n" .. n .. " = " .. val.value .. " ~= nil and tostring( " .. val.value .. " ) or nil";
initialise = nil;
dependants = {};
dependencies = { val };
}
state.names[n] = "n" .. n
val.dependants[#val.dependants + 1] = t
state.tostringed = true
return t
elseif value.type == "tag" then
local val = dynamic_value_internal( value.value, state )
local nidx = #state.names + 1
local nval = #state.names + 2
local npdt = #state.names + 3
local t = {
value = "n" .. nval;
complex = true;
update = "f" .. npdt .. "()";
initialise = nil;
dependants = {};
dependencies = { val };
}
state.names[nidx] = "n" .. nidx
state.names[nval] = "n" .. nval
state.names[npdt] = "f" .. npdt
state.functions[#state.functions + 1] = {
code = TAG_CHECK_UPDATER
:gsub( "NAME", "n" .. nidx )
:gsub( "TAG", ("%q"):format( value.tag ) )
:gsub( "FUNC", "f" .. npdt )
:gsub( "LVALUE", val.value )
:gsub( "VALUE", "n" .. nval );
node = t;
}
val.dependants[#val.dependants + 1] = t
return t
else
-- TODO: every other type of node
error "TODO: fix this error"
end
end
local is_operator = {
["+"] = true;
["-"] = true;
["*"] = true;
["/"] = true;
["%"] = true;
["^"] = true;
["&"] = true;
["|"] = true;
[">"] = true;
["<"] = true;
[">="] = true;
["<="] = true;
["!="] = true;
["=="] = true;
}
local op_precedences = {
["|"] = 0;
["&"] = 1;
["!="] = 2;
["=="] = 2;
[">"] = 3;
["<"] = 3;
[">="] = 3;
["<="] = 3;
["+"] = 4;
["-"] = 4;
["*"] = 5;
["/"] = 5;
["%"] = 5;
["^"] = 6;
}
local lua_operators = {
["|"] = "or";
["&"] = "and";
["!="] = "~=";
}
local function parse_name( stream )
return stream:skip_value( "identifier" )
end
DynamicValueParser = class.new( "DynamicValueParser", nil, nil ) {
stream = nil;
flags = {};
}
function DynamicValueParser:DynamicValueParser( stream )
self.stream = stream
self.flags = {}
end
function DynamicValueParser:parse_primary_expression()
if self.stream:skip( "float", "self" ) then
return { type = "self" }
elseif self.stream:skip( "float", "application" ) then
return { type = "application" }
elseif self.stream:skip( "float", "parent" ) then
return { type = "parent" }
elseif self.stream:test( "identifier" ) then
return { type = "identifier", value = parse_name( self.stream ) }
elseif self.stream:test( "int" ) then
return { type = "integer", value = self.stream:next().value }
elseif self.stream:test( "float" ) then
return { type = "float", value = self.stream:next().value }
elseif self.stream:test( "float" ) then
return { type = "boolean", value = self.stream:next().value }
elseif self.stream:test( "string" ) then
return { type = "string", value = self.stream:next().value }
elseif self.stream:test( "symbol", "$" ) then
if self.flags.enable_queries then
self.stream:next()
else
error "TODO: fix this error"
end
local dynamic = not self.stream:skip( "symbol", "$" )
local query = self:parse_query_term( true )
return { type = dynamic and "dynamic query" or "query", query = query, source = { type = "application" } }
elseif self.stream:skip( "symbol", "(" ) then
local expr = self:parse_expression() or error "TODO: fix this error"
return self.stream:skip( "symbol", ")" ) and expr or error "TODO: fix this error"
end
return nil
end
function DynamicValueParser:parse_term()
local operators = {}
while self.stream:test( "symbol", "#" )
or self.stream:test( "symbol", "!" )
or self.stream:test( "symbol", "-" )
or self.stream:test( "symbol", "+" ) do
operators[#operators + 1] = self.stream:next().value
end
local term = self:parse_primary_expression()
while term do
if self.stream:skip( "symbol", "." ) then
local index = parse_name( self.stream )
or self.stream:skip_value( "float", "parent" )
or self.stream:skip_value( "float", "application" )
or error "TODO: fix this error"
term = { type = "dotindex", value = term, index = index }
elseif self.stream:skip( "symbol", "#" ) then
local tag = parse_name( self.stream )
or self.stream:skip_value( "float" )
or error "TODO: fix this error"
term = { type = "tag", value = term, tag = tag }
elseif self.stream:skip( "symbol", "(" ) then
local parameters = {}
while self.stream:skip( "whitespace" ) do end
if not self.stream:skip( "symbol", ")" ) then
repeat
while self.stream:skip( "whitespace" ) do end
parameters[#parameters + 1] = self:parse_expression() or error "TODO: fix this error"
while self.stream:skip( "whitespace" ) do end
until not self.stream:skip( "symbol", "," )
if not self.stream:skip( "symbol", ")" ) then
error "TODO: fix this error"
end
end
term = { type = "call", value = term, parameters = parameters }
elseif self.stream:skip( "symbol", "[" ) then
while self.stream:skip( "whitespace" ) do end
local index = self:parse_expression() or error "TODO: fix this error"
while self.stream:skip( "whitespace" ) do end
if not self.stream:skip( "symbol", "]" ) then
error "TODO: fix this error"
end
term = { type = "index", value = term, index = index }
elseif self.stream:test( "symbol", "$" ) then
if self.flags.enable_queries then
self.stream:next()
else
error "TODO: fix this error"
end
local dynamic = not self.stream:skip( "symbol", "$" )
local query = self:parse_query_term( true )
term = { type = dynamic and "dynamic query" or "query", query = query, source = term }
elseif self.stream:test( "symbol", "%" ) then
if self.flags.enable_percentages then
self.stream:next()
else
error "TODO: fix this error"
end
term = { type = "percentage", value = term }
else
break
end
end
for i = #operators, 1, -1 do
term = term and { type = "unary operator expression", value = term, operator = operators[i] }
end
return term
end
function DynamicValueParser:parse_expression()
local operand_stack = { self:parse_term() }
local operator_stack = {}
local precedences = {}
if #operand_stack == 0 then
return nil
end
while self.stream:skip( "whitespace" ) do end
while self.stream:test( "symbol" ) and is_operator[self.stream:peek().value] do
local op = self.stream:next().value
local prec = op_precedences[op]
while precedences[1] and precedences[#precedences] >= prec do
local rvalue = table.remove( operand_stack, #operand_stack )
table.remove( precedences, #precedences )
operand_stack[#operand_stack] = {
type = "binary operator expression";
operator = table.remove( operator_stack, #operator_stack );
lvalue = operand_stack[#operand_stack];
rvalue = rvalue;
}
end
while self.stream:skip( "whitespace" ) do end
operand_stack[#operand_stack + 1] = self:parse_term() or error "TODO: fix this"
operator_stack[#operator_stack + 1] = lua_operators[op] or op
precedences[#precedences + 1] = prec
while self.stream:skip( "whitespace" ) do end
end
while precedences[1] do
local rvalue = table.remove( operand_stack, #operand_stack )
table.remove( precedences, #precedences )
operand_stack[#operand_stack] = {
type = "binary operator expression";
operator = table.remove( operator_stack, #operator_stack );
lvalue = operand_stack[#operand_stack];
rvalue = rvalue;
}
end
return operand_stack[1]
end
function DynamicValueParser:parse_query_term( in_dynamic_value )
local negation_count, obj = 0
while self.stream:skip( "symbol", "!" ) do
negation_count = negation_count + 1
end
if self.stream:test( "identifier" ) or self.stream:skip( "symbol", "#" ) then -- ID
obj = { type = "id", value = parse_name( self.stream ) or error "TODO: fix this error" }
ID_parsed = true
elseif self.stream:skip( "symbol", "*" ) then
obj = { type = "any" }
elseif self.stream:skip( "symbol", "?" ) then
obj = { type = "class", value = parse_name( self.stream ) or error "TODO: fix this error" }
elseif self.stream:skip( "symbol", "(" ) then
print( self.stream:peek().value )
obj = self:parse_query()
if not self.stream:skip( "symbol", ")" ) then
error "TODO: fix this error"
end
end
local tags = {}
while (not in_dynamic_value or not obj) and self.stream:skip( "symbol", "." ) do -- tags
local tag = { type = "tag", value = parse_name( self.stream ) or error "TODO: fix this error" }
if obj then
obj = { type = "operator", operator = "&", lvalue = obj, rvalue = tag }
else
obj = tag
end
end
if self.stream:skip( "symbol", "[" ) then
local attributes = {}
repeat
local name = parse_name( self.stream ) or error "TODO: fix this error"
while self.stream:skip( "whitespace" ) do end
local comparison
= self.stream:skip_value( "symbol", "=" )
or self.stream:skip_value( "symbol", ">" )
or self.stream:skip_value( "symbol", "<" )
or self.stream:skip_value( "symbol", ">=" )
or self.stream:skip_value( "symbol", "<=" )
or self.stream:skip_value( "symbol", "!=" )
or error "TODO: fix this"
while self.stream:skip( "whitespace" ) do end
local value = self:parse_expression() or error "TODO: fix this error"
attributes[#attributes + 1] = {
name = name;
comparison = comparison;
value = value;
}
until not self.stream:skip( "symbol", "," )
if not self.stream:skip( "symbol", "]" ) then
error "TODO: fix this error"
end
obj = obj and {
type = "operator";
rvalue = obj;
lvalue = { type = "attributes", attributes = attributes };
operator = "&";
} or { type = "attributes", attributes = attributes }
end
if not obj then
error "TODO: fix this error"
end
if negation_count % 2 == 1 then
obj = { type = "negate", value = obj }
end
return obj
end
function DynamicValueParser:parse_query( in_dynamic_value )
local operands = { self:parse_query_term( in_dynamic_value ) }
local operators = {}
while self.stream:skip( "whitespace" ) do end
while self.stream:test( "symbol" ) do
local prec = operator_list[self.stream:peek().value]
if prec then
while operators[1] and operator_list[operators[#operators]] >= prec do -- assumming left associativity for all operators
operands[#operands - 1] = {
type = "operator";
lvalue = operands[#operands - 1];
rvalue = table.remove( operands, #operands );
operator = table.remove( operators, #operators );
}
end
operators[#operators + 1] = self.stream:next().value
while self.stream:skip( "whitespace" ) do end
operands[#operands + 1] = self:parse_query_term( in_dynamic_value )
while self.stream:skip( "whitespace" ) do end
else
break
end
end
while operators[1] do
operands[#operands - 1] = {
type = "operator";
lvalue = operands[#operands - 1];
rvalue = table.remove( operands, #operands );
operator = table.remove( operators, #operators );
}
end
return operands[1]
end
QueryTracker=class.new("QueryTracker",nil,nil){
parent=nil;
queries={};
lifetimes={};
subscriptions={};
ID=0;
}
function QueryTracker:QueryTracker(parent)
self.parent=parent
self.queries={}
self.lifetimes={}
self.subscriptions={}
end
function QueryTracker:track(query,nodes)
local ID=self.ID
self.queries[#self.queries+1]={query,nodes,ID}
self.ID=ID+1
self.lifetimes[ID]={}
return ID
end
function QueryTracker:is_tracking(ID)
for i=1,#self.queries do
if self.queries[i][3]==ID then
return true
end
end
return false
end
function QueryTracker:get_query(ID)
for i=1,#self.queries do
if self.queries[i][3]==ID then
return self.queries[i]
end
end
end
function QueryTracker:untrack(ID)
for i=#self.queries,1,-1 do
if self.queries[i][3]==ID then
local t=self.lifetimes[ID]
for i=#t,1,-1 do
local l=t[i]
t[i]=nil
if l[1]=="value"then
l[2].values:unsubscribe(l[3],l[4])
elseif l[1]=="query"then
l[2]:unsubscribe(l[3],l[4])
elseif l[1]=="tag"then
l[2]:unsubscribe_from_tag(l[3],l[4])
end
end
self.lifetimes[ID]=nil
self.subscriptions[ID]=nil
return table.remove(self.queries,i)
end
end
end
function QueryTracker:subscribe(ID,lifetime,callback)
local t=self.subscriptions[ID]or{}
lifetime[#lifetime+1]={"query",self,ID,callback}
self.subscriptions[ID]=t
t[#t+1]=callback
end
function QueryTracker:unsubscribe(ID,callback)
if self.subscriptions[ID]then
for i=#self.subscriptions[ID],1,-1 do
if self.subscriptions[ID][i]==callback then
if#self.subscriptions[ID]==1 then
self:untrack(ID)
else
table.remove(self.subscriptions[ID],i)
end
return callback
end
end
end
end
function QueryTracker:update(mode,child)
for i=1,#self.queries do
local add,remove=(mode=="child-added"or mode=="child-changed")and self.queries[i][1](child),mode=="child-removed"
if mode=="child-changed"and not add then
remove=true
end
if add then
local nodes=self.queries[i][2]
local collated=self.parent.collated_children
local n=1
for i=1,#collated do
if collated[i]==child then
break
elseif collated[i]==nodes[n]then
n=n+1
end
end
if nodes[n]~=child then--if it's not already in the query
table.insert(nodes,n,child)
self:invoke_child_change(self.queries[i][3],child,"child-added")
else
self:invoke_child_change(self.queries[i][3],child,"child-changed")
end
elseif remove then
local t=self.queries[i][2]
for n=1,#t do
if t[n]==child then
table.remove(t,n)
self:invoke_child_change(self.queries[i][3],child,"child-removed")
break
end
end
end
end
end
function QueryTracker:invoke_child_change(query_ID,child,mode)
local callbacks=self.subscriptions[query_ID]or{}
for i=1,#callbacks do
callbacks[i](mode,child)
end
end
local escape_chars={
["n"]="\n";
["r"]="\r";
["t"]="\t";
}
local symbols={
["("]=true;[")"]=true;
["["]=true;["]"]=true;
["{"]=true;["}"]=true;
["."]=true;[":"]=true;
[","]=true;[";"]=true;
["="]=true;
["$"]=true;
["+"]=true;["-"]=true;
["*"]=true;["/"]=true;
["%"]=true;["^"]=true;
["#"]=true;
["!"]=true;
["&"]=true;["|"]=true;
["?"]=true;
[">"]=true;["<"]=true;
[">="]=true;["<="]=true;
["!="]=true;["=="]=true;
}
local keywords={
["self"]=true;
["application"]=true;
["parent"]=true;
}
Stream=class.new("Stream",nil,nil){
position=1;
line=1;
character=1;
text="";
}
function Stream:Stream(text)
self.text=text
end
function Stream:consume_string()
local text=self.text
local close=text:sub(self.position,self.position)
local escaped=false
local sub=string.sub
local str={}
for i=self.position+1,#text do
local char=sub(text,i,i)
if char=="\n"then
self.line=self.line+1
self.character=0
end
if escaped then
str[#str+1]=escape_chars[char]or"\\"..char
elseif char=="\\"then
escaped=true
elseif char==close then
self.position=i+1
return{type="string",value=table.concat(str),position={
character=char,line=line;
}}
else
str[#str+1]=char
end
self.character=self.character+1
end
error("TODO:fix this error")
end
function Stream:consume_identifier()
local word=self.text:match("[%w_]+",self.position)
local char=self.character
local type=keywords[word]and"float"
or(word=="true"or word=="false"and"float")
or"identifier"
self.position=self.position+#word
self.character=self.character+#word
return{type=type,value=word,position={
character=char,line=self.line;
}}
end
function Stream:consume_number()
local char=self.character
local num=self.text:match("%d*%.?%d+e[%+%-]?%d+",self.position)
or self.text:match("%d*%.?%d+",self.position)
local type=(num:find"%."or num:find"e%-")
and"float"or"int"
self.position=self.position+#num
self.character=self.character+#num
return{type=type,value=num,position={
character=char,line=line;
}}
end
function Stream:consume_whitespace()
local line,char=self.line,self.character
local type="whitespace"
local value="\n"
if self.text:sub(1,1)=="\n"then
self.line=self.line+1
self.position=self.position+1
self.character=1
type="newline"
else
local n=#self.text:match("^[^%S\n]+",self.position)
value=self.text:sub(self.position,self.position+n-1)
self.position=self.position+n
self.character=self.character+n
end
return{type=type,value=value,position={
character=char,line=line;
}}
end
function Stream:consume_symbol()
local text=self.text
local sub=string.sub
local pos=self.position
local s3=sub(text,pos,pos+2)
local s2=sub(text,pos,pos+1)
local s1=sub(text,pos,pos+0)
local value=s1
local char=self.character
if symbols[s3]then
value=s3
elseif symbols[s2]then
value=s2
elseif not symbols[s1]then
print(s1,s2,s3)
error("TODO:fix this error")
end
self.character=self.character+#value
self.position=self.position+#value
return{type="symbol",value=value,position={
character=char,line=self.line;
}}
end
function Stream:consume()
if self.position>#self.text then
return{type="eof",value="",position={
character=self.character,line=self.line;
}}
end
local char=self.text:sub(self.position,self.position)
if char=="\""or char=="'" then
return self:consume_string()
elseif char == "" or char == "\t" or char == "\n" then
return self:consume_whitespace()
elseif self.text:find( "^%.?%d", self.position ) then
return self:consume_number()
elseif char:find "%w" or char == "_" then
return self:consume_identifier()
else
return self:consume_symbol()
end
end
function Stream:is_EOF()
return self:peek().type == "eof"
end
function Stream:peek()
if self.buffer then
return self.buffer
end
local token = self:consume()
self.buffer = token
return token
end
function Stream:next()
local token = self:peek()
self.buffer = nil
return token
end
function Stream:test( type, value )
local token = self:peek()
return token.type == type and (value == nil or token.value == value) and token or nil
end
function Stream:skip( type, value )
local token = self:peek()
return token.type == type and (value == nil or token.value == value) and self:next() or nil
end
function Stream:skip_value( type, value )
local token = self:peek()
return token.type == type and (value == nil or token.value == value) and self:next().value or nil
end
Transition = class.new( "Transition", nil, nil ) {
duration = 0.4;
easing_function = nil;
}
function Transition:Transition( easing, duration )
self.easing_function = easing
self.duration = duration or self.duration
end
Transition.none = nil
Transition.linear = Transition( Easing.linear, 0.4 )
Transition.linear_slow = Transition( Easing.linear, 0.8 )
Transition.linear_fast = Transition( Easing.linear, 0.2 )
Transition.smooth = Transition( Easing.smooth, 0.4 )
Transition.smooth_slow = Transition( Easing.smooth, 0.8 )
Transition.smooth_fast = Transition( Easing.smooth, 0.2 )
Transition.entrance = Transition( Easing.entrance, 0.4 )
Transition.entrance_slow = Transition( Easing.entrance, 0.8 )
Transition.entrance_fast = Transition( Easing.entrance, 0.2 )
Transition.exit = Transition( Easing.exit, 0.4 )
Transition.exit_slow = Transition( Easing.exit, 0.8 )
Transition.exit_fast = Transition( Easing.exit, 0.2 )
Type = class.new( "Type", nil, nil ) {
name = "";
}
UnionType = class.new( "UnionType", Type, nil ) {
lvalue = nil;
rvalue = nil;
}
ListType = class.new( "ListType", Type, nil ) {
value = nil;
}
TableType = class.new( "TableType", Type, nil ) {
index = nil;
value = nil;
}
function Type:Type( name )
self.name = name
self.meta.__div = self.either
self.meta.__eq = self.matches
end
function UnionType:UnionType( lvalue, rvalue )
self.lvalue = lvalue
self.rvalue = rvalue
return self:Type "Union"
end
function ListType:ListType( value )
self.value = value
return self:Type "List"
end
function TableType:TableType( index, value )
self.index = index
self.value = value
return self:Type "Table"
end
function Type:tostring()
return self.name
end
function UnionType:tostring()
return self.lvalue:tostring() .. " | " .. self.rvalue:tostring()
end
function ListType:tostring()
return self.value:tostring() .. "[]"
end
function TableType:tostring()
return self.value:tostring() .. "{" .. self.index:tostring() .. "}"
end
function Type:either( other )
return UnionType( self, other )
end
function Type:matches( type )
if self:type_of( UnionType ) then
return self.lvalue:matches( type ) and self.rvalue:matches( type )
end
if type:type_of( UnionType ) then
return self:matches( type.lvalue ) or self:matches( type.rvalue )
elseif type:type_of( ListType ) then
return self.name == "List" and self.value:matches( type.value )
elseif type:type_of( TableType ) then
return self.name == "Table" and self.value:matches( type.value ) and self.index:matches( type.index )
elseif type.name == "Any" then
return true
else
return self.name == type.name
end
end
function Type:casts_to( type )
end
Type.primitive = {}
Type.primitive.null = Type "Null"
Type.primitive.integer = Type "Integer"
Type.primitive.number = Type "Number"
Type.primitive.string = Type "String"
Type.primitive.boolean = Type "Boolean"
Type.primitive.optional_integer = Type.primitive.integer / Type.primitive.null
Type.primitive.optional_number = Type.primitive.number / Type.primitive.null
Type.primitive.optional_string = Type.primitive.string / Type.primitive.null
Type.primitive.optional_boolean = Type.primitive.boolean / Type.primitive.null
Type.any = Type "Any"
Type.sheets = {}
Type.sheets.colour = Type "colour"
Type.sheets.alignment = Type "alignment"
Type.sheets.Sheet = Type "Sheet"
Type.sheets.optional_Sheet = Type "Sheet" / Type.primitive.null
Type.sheets.Screen = Type "Screen"
Type.sheets.Application = Type "Application"
Type.sheets.Sheet_or_Screen = Type.sheets.Sheet / Type.sheets.Screen
Typechecking = class.new( "Typechecking", nil, nil ) {
}
function Typechecking.check_type( ast, state )
if ast.type == "self" then
return ast, state.object:type_of( Sheet ) and Type.sheets.Sheet
or state.object:type_of( Screen ) and Type.sheets.Screen
or state.object:type_of( Application ) and Type.sheets.Application
or error "this really should never happen but just incase here's an error message"
elseif ast.type == "parent" then
return ast, state.object:type_of( Sheet ) and Type.sheets.Sheet_or_Screen
or state.object:type_of( Screen ) and Type.sheets.Application
or state.object:type_of( Application ) and Type.primitive.null
or error "this really should never happen but just incase here's another error message"
elseif ast.type == "call" then
return ast, error "TODO: implement calls, idk how but you can do this!"
elseif ast.type == "query" or ast.type == "dynamic query" then
local src, srctype = Typechecking.check_type( ast.source, state )
if not (srctype == Type.sheets.Sheet_or_Screen or srctype == Type.sheets.Application) then
error "TODO: fix this error"
end
ast.source = src
return ast, Type.sheets.Sheet_or_Screen
elseif ast.type == "string" then
return ast, Type.primitive.string
elseif ast.type == "integer" then
return ast, Type.primitive.integer
elseif ast.type == "float" then
return ast, Type.primitive.number
elseif ast.type == "boolean" then
return ast, Type.primitive.boolean
elseif ast.type == "index" then
return ast, error "TODO: implement indexes, idk how but you can do this!"
elseif ast.type == "application" then
return ast, Type.sheets.Application
elseif ast.type == "identifier" then
if state.environment[ast.value] then
return ast, state.environment[ast.value].type
elseif state.object.values:has( ast.value ) then
return {
type = "dotindex";
value = {
type = "self";
};
index = ast.value;
}, ValueHandler.properties[ast.value].type
else
error "TODO: fix this error"
end
elseif ast.type == "unary operator expression" then
local _ast, type = Typechecking.check_type( ast.value, state )
ast.value = _ast
if ast.operator == "#" then
if not (type == ListType( Type.any ) or type == Type.primitive.string) then
error "TODO: fix this error"
end
type = Type.primitive.integer
elseif ast.operator == "!" then
-- any type is fine
elseif ast.operator == "-" or ast.operator == "+" then
if not (type == Type.primitive.integer or type == Type.primitive.number) then
error "TODO: fix this error"
end
end
return ast, type
elseif ast.type == "dotindex" then
local _ast, vtype = Typechecking.check_type( ast.value, state )
ast.value = _ast
if ValueHandler.properties[ast.index] then
if vtype == (Type.sheets.Sheet_or_Screen / Type.sheets.Application / Type.primitive.null) then
return ast, ValueHandler.properties[ast.index].type -- do a check for the index
else
error "TODO: fix this error"
end
else
error "TODO: fix this error"
end
return ast
elseif ast.type == "percentage" then
-- See issue #37
elseif ast.type == "binary operator expression" then
local lvalue_ast, lvalue_type = Typechecking.check_type( ast.lvalue, state )
local rvalue_ast, rvalue_type = Typechecking.check_type( ast.rvalue, state )
ast.lvalue = lvalue_ast
ast.rvalue = rvalue_ast
if ast.operator == "+" then
if lvalue_type == Type.primitive.string then
if not (rvalue_type == Type.primitive.string or rvalue_type == Type.primitive.integer or rvalue_type == Type.primitive.number) then
-- tostring it
error "TODO: implement this"
end
ast.operator = ".."
return ast, Type.primitive.string
elseif rvalue_type == Type.primitive.string then
if not (lvalue_type == Type.primitive.string or lvalue_type == Type.primitive.integer or lvalue_type == Type.primitive.number) then
-- tostring it
error "TODO: implement this"
end
ast.operator = ".."
elseif lvalue_type == Type.primitive.integer then
if rvalue_type == Type.primitive.integer then
return ast, Type.primitive.integer
elseif rvalue_type == Type.primitive.number then
return ast, Type.primitive.number
else
error "TODO: fix this error"
end
elseif lvalue_type == Type.primitive.number then
if rvalue_type == Type.primitive.integer or rvalue_type == Type.primitive.number then
return ast, Type.primitive.number
else
error "TODO: fix this error"
end
else
error "TODO: fix this error"
end
elseif ast.operator == "-" or ast.operator == "*" or ast.operator == "^" then
if lvalue_type == Type.primitive.integer then
if rvalue_type == Type.primitive.integer then
return ast, Type.primitive.integer
elseif rvalue_type == Type.primitive.number then
return ast, Type.primitive.number
else
error "TODO: fix this error"
end
elseif lvalue_type == Type.primitive.number then
if rvalue_type == Type.primitive.integer or rvalue_type == Type.primitive.number then
return ast, Type.primitive.number
else
error "TODO: fix this error"
end
else
error "TODO: fix this error"
end
elseif ast.operator == "/" then
if lvalue_type == Type.primitive.integer / Type.primitive.number
and rvalue_type == Type.primitive.integer / Type.primitive.number then
return ast, Type.primitive.number
else
error "TODO: fix this error"
end
elseif ast.operator == "%" then
if lvalue_type == Type.primitive.integer and rvalue_type == Type.primitive.integer then
return ast, Type.primitive.number
else
error "TODO: fix this error"
end
elseif ast.operator == "and" then
return ast, rvalue_type / Type.primitive.null
elseif ast.operator == "or" then
local tr = { lvalue_type }
local idx = 1
while tr[idx] do
local t = tr[idx]
if t:type_of( UnionType ) then
if not (t.lvalue == Type.primitive.null) then
tr[idx] = t.lvalue
if not (t.rvalue == Type.primitive.null) then
table.insert( tr, idx, t.rvalue )
end
elseif not (t.rvalue == Type.primitive.null) then
tr[idx] = t.rvalue
else
table.remove( tr, idx )
end
else
idx = idx + 1
end
end
local t = tr[1]
if t then
for i = 2, #tr do
t = UnionType( t, tr[i] )
end
return ast, t / rvalue_type
else
return rvalue_ast, rvalue_type
end
elseif ast.operator == ">" or ast.operator == "<" or ast.operator == ">=" or ast.operator == "<=" then
if lvalue_type == Type.primitive.integer / Type.primitive.number
and rvalue_type == Type.primitive.integer / Type.primitive.number then
return Type.primitive.boolean
else
error "TODO: fix this error"
end
elseif ast.operator == "~=" or ast.operator == "==" then
return type.primitive.boolean
end
elseif ast.type == "tag" then
local obj, objtype = Typechecking.check_type( ast.value, state )
ast.value = obj
if objtype == Type.sheets.Sheet_or_Screen / Type.primitive.null then
return ast, Type.primitive.boolean
else
error "TODO: fix this error"
end
end
end
function Typechecking.resolve_type( value )
local t = type( value )
if t == "number" then
return value % 1 == 0 and Type.primitive.integer or Type.primitive.number
elseif t == "boolean" or t == "string" then
return Type.primitive[t]
elseif t == "nil" then
return Type.primitive.null
-- potentially add tables here
else
return Type.any
end
end
local floor = math.floor
local get_transition_function
local TRANSITION_FUNCTION_CODE
local tfcache = {}
local setf
ValueHandler = class.new( "ValueHandler", nil, nil ) {
object = nil;
lifetimes = {};
values = {};
subscriptions = {};
defaults = {};
removed_lifetimes = {};
transitions = {};
transitions_lookup = {};
}
ValueHandler.properties = {}
function ValueHandler:ValueHandler( object )
self.object = object
self.lifetimes = {}
self.values = {}
self.subscriptions = {}
self.defaults = {}
self.removed_lifetimes = {}
self.transitions = {}
self.transitions_lookup = {}
object.set = setf
end
function ValueHandler:add( name, default, options )
if not ValueHandler.properties[name] then
error "TODO: fix this error"
end
self.object["set_" .. name] = type( options ) == "function" and options or Codegen.dynamic_property_setter( name, options )
self.object["raw_" .. name] = default
self.object[name] = default
self.values[#self.values + 1] = name
self.defaults[name] = default
self.lifetimes[name] = {}
if ValueHandler.properties[name].transitionable then
self.object["set_" .. name .. "_transition"] = get_transition_function( name )
self.object[name .. "_transition"] = Transition.none
end
end
function ValueHandler:remove( name )
self.lifetimes[name] = nil
self.values[name] = nil
self.object[name] = nil
self.object["raw_" .. name] = nil
self.object["set_" .. name] = nil
end
function ValueHandler:has( name )
return self.lifetimes[name] ~= nil
end
function ValueHandler:trigger( name )
if self.subscriptions[name] then
for i = #self.subscriptions[name], 1, -1 do
self.subscriptions[name][i]()
end
end
end
function ValueHandler:respawn( name )
local t = self.lifetimes[name]
for i = #t, 1, -1 do
local l = t[i]
t[i] = nil
if l[1] == "value" then
l[2].values:unsubscribe( l[3], l[4] )
elseif l[1] == "query" then
l[2]:unsubscribe( l[3], l[4] )
elseif l[1] == "tag" then
l[2]:unsubscribe_from_tag( l[3], l[4] )
end
end
self.lifetimes[name] = {}
end
function ValueHandler:subscribe( name, lifetime, callback )
self.subscriptions[name] = self.subscriptions[name] or {}
lifetime[#lifetime + 1] = { "value", self.object, name, callback }
local t = self.subscriptions[name]
t[#t + 1] = callback
return callback
end
function ValueHandler:unsubscribe( name, callback )
if self.subscriptions[name] then
for i = #self.subscriptions[name], 1, -1 do
if self.subscriptions[name][i] == callback then
table.remove( self.subscriptions[name], i )
return callback
end
end
end
end
function ValueHandler:child_removed()
for k, v in pairs( self.lifetimes ) do
self.removed_lifetimes[k] = v
self:respawn( k )
end
end
function ValueHandler:child_inserted()
for k, v in pairs( self.removed_lifetimes ) do
local lifetime = self.lifetimes[k]
for i = 1, #v do
if v[i][1] == "value" then
v[i][2].values:subscribe( v[i][3], lifetime, v[i][4] )
elseif v[i][1] == "query" then
v[i][2]:subscribe( v[i][3], lifetime, v[i][4] )
end
v[i][4]()
end
end
self.removed_lifetimes = {}
end
function ValueHandler:transition( property, final, transition, custom_update )
local index = self.transitions_lookup[property] or #self.transitions + 1
local floored = false -- TODO: make this respect the property
local ptype = ValueHandler.properties[property].type
if ptype == Type.primitive.integer then
floored = true
elseif ptype ~= Type.primitive.number then
Exception.throw( Exception( "PropertyTransitionException", "Cannot animate non-number property '" .. property .. "'" ) ) -- TODO: make custom exception for this
end
final = floored and floor( final + 0.5 ) or final
if transition ~= Transition.none and self.object.application then
self.transitions_lookup[property] = index
self.transitions[index] = {
property = property;
initial = self.object[property];
final = final;
diff = final - self.object[property];
duration = transition.duration;
clock = 0;
easing = transition.easing_function;
floored = floored;
change = ValueHandler.properties[property].change;
custom_update = custom_update;
}
else
if self.object[property] ~= final then
self.object[property] = final
if ValueHandler.properties[property].change == "self" then
self.object:set_changed()
elseif ValueHandler.properties[property].change == "parent" and self.object.parent then
self.object.parent:set_changed()
end
if custom_update then
custom_update( self.object )
end
self:trigger( property )
end
end
end
function ValueHandler:update( dt )
for i = #self.transitions, 1, -1 do
local trans = self.transitions[i]
trans.clock = trans.clock + dt
if trans.clock >= trans.duration then
self.object[trans.property] = trans.final
table.remove( self.transitions, i )
self.transitions_lookup[trans.property] = nil
else
local eased = trans.easing( trans.initial, trans.diff, trans.clock / trans.duration )
self.object[trans.property] = trans.floored and floor( eased + 0.5 ) or eased
end
if trans.change == "self" then
self.object:set_changed()
elseif trans.change == "parent" and self.object.parent then
self.object.parent:set_changed()
end
if trans.custom_update then
trans.custom_update( self.object )
end
self:trigger( trans.property )
end
end
ValueHandler.properties.x = { type = Type.primitive.integer, change = "parent", transitionable = true }
ValueHandler.properties.y = { type = Type.primitive.integer, change = "parent", transitionable = true }
ValueHandler.properties.z = { type = Type.primitive.integer, change = "parent", transitionable = true }
ValueHandler.properties.x_offset = { type = Type.primitive.integer, change = "self", transitionable = true }
ValueHandler.properties.y_offset = { type = Type.primitive.integer, change = "self", transitionable = true }
ValueHandler.properties.width = { type = Type.primitive.integer, change = "self", transitionable = true }
ValueHandler.properties.height = { type = Type.primitive.integer, change = "self", transitionable = true }
ValueHandler.properties.text = { type = Type.primitive.string, change = "self", transitionable = false }
ValueHandler.properties.horizontal_alignment = { type = Type.sheets.alignment, change = "self", transitionable = false }
ValueHandler.properties.vertical_alignment = { type = Type.sheets.alignment, change = "self", transitionable = false }
ValueHandler.properties.colour = { type = Type.sheets.colour, change = "self", transitionable = false }
ValueHandler.properties.text_colour = { type = Type.sheets.colour, change = "self", transitionable = false }
ValueHandler.properties.active_colour = { type = Type.sheets.colour, change = "self", transitionable = false }
ValueHandler.properties.parent = { type = Type.sheets.optional_Sheet, change = "parent", transitionable = false }
function get_transition_function( name )
if not tfcache[name] then
tfcache[name] = (load or loadstring)( TRANSITION_FUNCTION_CODE:gsub( "PROPERTY", name ) )()
end
return tfcache[name]
end
function setf( self, t )
for k, v in pairs( t ) do
if self["set_" .. k] then
self["set_" .. k]( self, v )
else
-- TODO: error or just ignore?
end
end
return self
end
TRANSITION_FUNCTION_CODE = [[
return function( self, value )
self.PROPERTY_transition = value
return self
end]]
local function exception_handler( e )
return error( tostring( e ), 0 )
end
local handle_event
Application = class.new( "Application", nil, IQueryable, ITimer ) {
name = "UnNamed Application";
path = "";
terminateable = true;
running = true;
screen = nil;
-- internal
screens = {};
resource_loaders = {};
extensions = {};
mouse = nil;
keys = {};
changed = false;
}
function Application:Application( name, path )
self.name = name
self.path = path or name
self.screens = {}
self.resource_loaders = {}
self.extensions = {}
self.keys = {}
self:ICollatedChildren()
self:IQueryable()
self:ITimer()
self.screens[1] = Screen( self, term.getSize() ):add_terminal( term )
self.screen = self.screens[1]
end
function Application:register_resource_loader( type, loader )
parameters.check( 2, "type", "string", type, "loader", "function", loader )
self.resource_loaders[type] = loader
end
function Application:unregister_resource_loader( type )
parameters.check( 1, "type", "string", type )
self.resource_loaders[type] = nil
end
function Application:register_file_extension( extension, type )
parameters.check( 2, "extension", "string", extension, "type", "string", type )
self.extensions[extension] = type
end
function Application:unregister_file_extension( extension )
parameters.check( 1, "extension", "string", extension )
self.extensions[extension] = nil
end
function Application:load_resource( resource, type, ... )
parameters.check( 2, "resource", "string", resource, "type", "string", type or "" )
if not type then
type = self.extensions[resource:match( "%.(%w+)$" ) or "txt"] or "text.plain"
end
if self.resource_loaders[type] then
local h = fs.open( fs.combine( self.path, resource ), "r" ) or fs.open( resource, "r" )
if h then
local content = h.readAll()
h.close()
return self.resource_loaders[type]( self, resource, content, ... )
else
Exception.throw( ResourceLoadException, "Failed to open file'" .. resource .. "':not found under'/''or'" .. self.path .. "'", 2 )
end
else
Exception.throw( ResourceLoadException, "No loader for resource type'" .. type .. "'", 2 )
end
end
function Application:is_key_pressed( key )
parameters.check( 1, "key", "string", key )
return self.keys[key] ~= nil
end
function Application:stop()
self.running = false
return self
end
function Application:add_screen()
local screen = Screen( self, term.get_size() )
self.screens[#self.screens + 1] = screen
return screen
end
function Application:remove_screen( screen )
parameters.check( 1, "screen", Screen, screen )
for i = #self.screens, 1, -1 do
if self.screens[i] == screen then
return table.remove( self.screens, i )
end
end
end
function Application:child_value_changed( child )
return self.query_tracker:update( "child-changed", child )
end
function Application:update_collated( mode, child, data )
local collated = self.collated_children
if mode == "child-added" then
if data:type_of( Screen ) then
local v = data
data = nil
for i = 1, #self.screens do
if self.screens[i] == v then
repeat
i = i + 1
until not self.screens[i] or #self.screens[i].collated_children > 0
if self.screens[i] then
data = self.screens[i].collated_children[1]
end
break
end
end
end
if data then
for i = #collated, 1, -1 do
if collated[i] == data then
if child:implements( ICollatedChildren ) then
i = i - 1
for n = 1, #child.collated_children do
table.insert( collated, i + n, child.collated_children[n] )
end
table.insert( collated, i + #child.collated_children + 1, child )
else
table.insert( collated, i, child )
end
end
end
else
if child:implements( ICollatedChildren ) then
for i = 1, #child.collated_children do
collated[#collated + 1] = child.collated_children[i]
end
end
collated[#collated + 1] = child
end
elseif mode == "child-removed" then
local open, close = child:implements( ICollatedChildren ) and child.collated_children[1] or child, child
local removing = false
for i = #collated, 1, -1 do
if collated[i] == close then removing = true end
local brk = collated[i] == open
if removing then table.remove( collated, i ) end
if brk then break end
end
end
self.query_tracker:update( mode, child )
end
function Application:event( event, ... )
local params = { ... }
if event == "timer" and self:update_timer( ... ) then
return
end
return handle_event( self, event, params, ... )
end
function Application:draw()
if self.changed then
for i = 1, #self.screens do
self.screens[i]:draw()
end
self.changed = false
end
end
function Application:update()
local dt = self:step_timer():get_timer_delta()
for i = 1, #self.screens do
self.screens[i]:update( dt )
end
if self.on_update then
self:on_update( dt )
end
end
function Application:load()
self.changed = true
if self.on_load then
return self:on_load()
end
end
function Application:run()
Exception.try (function()
self:load()
local t = os.startTimer( 0 ) -- updating timer
while self.running do
local event = { coroutine.yield() }
if event[1] == "timer" and event[2] == t then
t = os.startTimer( .05 )
elseif event[1] == "terminate" and self.terminateable then
self:stop()
else
self:event( unpack( event ) )
end
self:update()
self:draw()
end
end) {
Exception.default (exception_handler);
}
end
function handle_event( self, event, params, ... )
local screens = {}
for i = 1, #self.screens do
screens[i] = self.screens[i]
end
if event == "mouse_click" then
self.mouse = {
x = params[2] - 1, y = params[3] - 1;
down = true, button = params[1];
timer = os.startTimer( 1 ), time = os.clock(), moved = false;
}
local e = MouseEvent( 0, params[2] - 1, params[3] - 1, params[1], true )
for i = #screens, 1, -1 do
if screens[i]:gets_term_events() then
screens[i]:handle( e )
end
end
elseif event == "mouse_up" then
local e = MouseEvent( 1, params[2] - 1, params[3] - 1, params[1], true )
for i = #screens, 1, -1 do
if screens[i]:gets_term_events() then
screens[i]:handle( e )
end
end
self.mouse.down = false
os.cancelTimer( self.mouse.timer )
if not self.mouse.moved and os.clock() - self.mouse.time < 1 and params[1] == self.mouse.button then
local e = MouseEvent( 2, params[2] - 1, params[3] - 1, params[1], true )
for i = #screens, 1, -1 do
if screens[i]:gets_term_events() then
screens[i]:handle( e )
end
end
end
elseif event == "mouse_drag" then
local e = MouseEvent( 4, params[2] - 1, params[3] - 1, params[1], true )
for i = #screens, 1, -1 do
if screens[i]:gets_term_events() then
screens[i]:handle( e )
end
end
self.mouse.moved = true
os.cancelTimer( self.mouse.timer )
elseif event == "mouse_scroll" then
local e = MouseEvent( 5, params[2] - 1, params[3] - 1, params[1], true )
for i = #screens, 1, -1 do
if screens[i]:gets_term_events() then
screens[i]:handle( e )
end
end
elseif event == "monitor_touch" then
local events = {
MouseEvent( 0, params[2] - 1, params[3] - 1, 1 );
MouseEvent( 1, params[2] - 1, params[3] - 1, 1 );
MouseEvent( 2, params[2] - 1, params[3] - 1, 1 );
}
for i = 1, #screens do
if screens[i]:uses_monitor( params[1] ) then
for n = 1, #events do
screens[i]:handle( events[n] )
end
end
end
elseif event == "chatbox_something" then
-- TODO: implement this
-- handle( TextEvent( 10, params[1] ) )
elseif event == "char" then
local e = TextEvent( 9, params[1] )
for i = #screens, 1, -1 do
screens[i]:handle( e )
end
elseif event == "paste" then
local e
if self.keys.leftShift or self.keys.rightShift then -- TODO: why the left_ctrl/right_ctrl?
e = KeyboardEvent( 7, keys.v, { left_ctrl = true, right_ctrl = true } )
else
e = TextEvent( 11, params[1] )
end
for i = #screens, 1, -1 do
screens[i]:handle( e )
end
elseif event == "key" then
self.keys[keys.getName( params[1] ) or params[1]] = os.clock()
local e = KeyboardEvent( 7, params[1], self.keys )
for i = #screens, 1, -1 do
screens[i]:handle( e )
end
elseif event == "key_up" then
self.keys[keys.getName( params[1] ) or params[1]] = nil
local e = KeyboardEvent( 8, params[1], self.keys )
for i = #screens, 1, -1 do
screens[i]:handle( e )
end
elseif event == "term_resize" then
local width, height = term.getSize()
for i = 1, #screens do
if screens[i].terminals[1] == term then
screens[i]:set_width( width )
screens[i]:set_height( height )
end
end
elseif event == "timer" and self.mouse and params[1] == self.mouse.timer then
local e = MouseEvent( 3, self.mouse.x, self.mouse.y, self.mouse.button, true )
for i = #screens, 1, -1 do
if screens[i]:gets_term_events() then
screens[i]:handle( e )
end
end
else
local ev = MiscEvent( event, ... )
for i = #screens, 1, -1 do
screens[i]:handle( ev )
end
if not ev.handled then
end
end
end
Screen = class.new( "Screen", nil, IChildContainer, ITagged, ISize ) {
parent = nil;
-- internal
terminals = {};
monitors = {};
surface = nil;
changed = true;
values = nil;
}
function Screen:Screen( application, width, height )
self.parent = application
self.terminals = {}
self.monitors = {}
self.surface = surface.create( 0, 0 )
self.application = application
self.values = ValueHandler( self )
self:ICollatedChildren()
self:IQueryable()
self:IChildContainer()
self:ITagged()
self:ISize()
self:set_width( width )
self:set_height( height )
end
function Screen:gets_term_events()
for i = 1, #self.terminals do
if self.terminals[i] == term then
return true
end
end
return false
end
function Screen:set_changed( state )
self.changed = state ~= false
if state ~= false then -- must have a parent Application
self.parent.changed = true
end
return self
end
function Screen:add_monitor( side )
parameters.check( 1, "side", "string", side )
if peripheral.getType( side ) ~= "monitor" then
throw( IncorrectParameterException, "expected monitor on side'" .. side .. "',got" .. peripheral.getType( side ), 2 )
end
local mon = peripheral.wrap( side )
self.monitors[side] = mon
return self:add_terminal( mon )
end
function Screen:remove_monitor( side )
parameters.check( 1, "side", "string", side )
local mon = self.monitors[side]
if mon then
self.monitors[side] = nil
self:remove_terminal( mon )
end
return self
end
function Screen:uses_monitor( side )
return self.monitors[side] ~= nil
end
function Screen:add_terminal( t )
parameters.check( 1, "terminal", "table", t )
self.terminals[#self.terminals + 1] = t
self.surface:clear()
return self:set_changed()
end
function Screen:remove_terminal( t )
parameters.check( 1, "terminal", "table", t )
for i = #self.terminals, 1, -1 do
if self.terminals[i] == t then
self:set_changed()
return table.remove( self.terminals, i )
end
end
return self
end
function Screen:draw()
if self.changed then
local surface = self.surface
local children = {}
local cx, cy, cc
surface:clear( 1 )
for i = 1, #self.children do
children[i] = self.children[i]
end
for i = 1, #children do
local child = children[i]
if child:is_visible() then
child:draw( self.surface, child.x, child.y )
if child.cursor_active then
cx, cy, cc = child.x + child.cursor_x, child.y + child.cursor_y, child.cursor_colour
end
end
end
for i = 1, #self.terminals do
surface:output( self.terminals[i] )
end
self.changed = false
for i = 1, #self.terminals do
if cx then
self.terminals[i].setCursorPos( cx + 1, cy + 1 )
self.terminals[i].setTextColour( cc )
self.terminals[i].setCursorBlink( true )
else
self.terminals[i].setCursorBlink( false )
end
end
end
end
function Screen:handle( event )
local c = {}
local children = self.children
for i = 1, #children do
c[i] = children[i]
end
if event:type_of( MouseEvent ) then
local within = event:is_within_area( 0, 0, self.width, self.height )
for i = #c, 1, -1 do
c[i]:handle( event:clone( c[i].x, c[i].y, within ) )
end
else
for i = #c, 1, -1 do
c[i]:handle( event )
end
end
end
function Screen:update( dt )
local children = {}
self.values:update( dt )
for i = 1, #self.children do
children[i] = self.children[i]
end
for i = 1, #children do
children[i]:update( dt )
end
end
Sheet = class.new( "Sheet", nil, ITagged, ISize ) {
x = 0;
y = 0;
z = 0;
style = nil;
parent = nil;
-- internal
changed = true;
cursor_x = 0;
cursor_y = 0;
cursor_colour = 0;
cursor_active = false;
handles_keyboard = false;
handles_text = false;
values = nil;
}
function Sheet:Sheet( x, y, width, height )
if x ~= nil then self:set_x( x ) end
if y ~= nil then self:set_y( y ) end
if width ~= nil then self:set_width( width ) end
if height ~= nil then self:set_height( height ) end
end
function Sheet:initialise()
self.values = ValueHandler( self )
self:ITagged()
self:ISize()
self.values:add( "x", 0 )
self.values:add( "y", 0 )
self.values:add( "z", 0, { custom_update_code = "if self.parent then self.parent:reposition_child_z_index(self)end" } )
self.values:add( "parent", nil, function( self, parent )
if parent and not class.type_of( parent, Sheet ) and not class.type_of( parent, Screen ) then
Exception.throw( IncorrectParameterException( "expected Sheet or Screen parent,got" .. class.type( parent ), 2 ) )
end
if parent then
return parent:add_child( self )
else
return self:remove()
end
end )
end
function Sheet:remove()
if self.parent then
return self.parent:remove_child( self )
end
end
function Sheet:is_visible()
return self.parent and self.parent:is_child_visible( self )
end
function Sheet:bring_to_front()
if self.parent then
return self:set_parent( self.parent ) -- TODO: improve this
end
return self
end
function Sheet:set_changed( state )
self.changed = state ~= false
if state ~= false and self.parent and not self.parent.changed then -- TODO: why not self.parent.changed?
self.parent:set_changed()
end
return self
end
function Sheet:set_cursor_blink( x, y, colour )
colour = colour or 128
parameters.check( 3, "x", "number", x, "y", "number", y, "colour", "number", colour )
self.cursor_active = true
self.cursor_x = x
self.cursor_y = y
self.cursor_colour = colour
return self
end
function Sheet:reset_cursor_blink()
self.cursor_active = false
return self
end
function Sheet:tostring()
return "[Instance]" .. self.class:type() .. "" .. tostring( self.id )
end
function Sheet:update( dt )
self.values:update( dt )
if self.on_update then
self:on_update( dt )
end
end
function Sheet:draw( surface, x, y )
self.changed = false
end
function Sheet:handle( event )
if event:type_of( MouseEvent ) then
if event:is( 6 ) and event:is_within_area( 0, 0, self.width, self.height ) and event.within then
event.button[#event.button + 1] = self
end
self:on_mouse_event( event )
elseif event:type_of( KeyboardEvent ) and self.handles_keyboard and self.on_keyboard_event then
self:on_keyboard_event( event )
elseif event:type_of( TextEvent ) and self.handles_text and self.on_text_event then
self:on_text_event( event )
end
end
function Sheet:on_mouse_event( event )
if not event.handled and event:is_within_area( 0, 0, self.width, self.height ) and event.within then
if event:is( 0 ) then
return event:handle( self )
end
end
end
Thread = class.new( "Thread", nil, nil ) {
running = true;
f = nil;
co = nil;
filter = nil;
}
function Thread:Thread( f, ... )
if type( f ) == "string" then
f = load( f )
elseif type( f ) ~= "function" then
parameters.check( 1, "f", "function/string", f )
end
self.f = f
self.co = coroutine.create( f )
self:resume( ... )
end
function Thread:stop()
self.running = false
end
function Thread:restart()
self.running = true
self.co = coroutine.create( self.f )
end
function Thread:resume( event, ... )
if not self.running or (self.filter ~= nil and event ~= self.filter) then
return
end
local ok, data = coroutine.resume( self.co, event, ... )
if ok then
if coroutine.status( self.co ) == "dead" then
self.running = false
end
self.filter = data
else
if data == "SHEETS_EXCEPTION\nPut code in a try block to catch the exception." then
return Exception.throw( Exception.thrown() )
end
return Exception.throw( ThreadRuntimeException, data, 0 )
end
end
-- needs to update to new exception system
Container = class.new( "Container", Sheet, IChildContainer, IColoured ) {
colour = nil;
x_offset = 0;
y_offset = 0;
on_pre_draw = nil;
on_post_draw = nil;
}
function Container:Container( x, y, w, h )
self:initialise()
self:ICollatedChildren()
self:IQueryable()
self:IChildContainer()
self:IColoured()
self.values:add( "x_offset", 0 )
self.values:add( "y_offset", 0 )
return self:Sheet( x, y, w, h )
end
function Container:update( dt )
local children = self:get_children()
self.values:update( dt )
if self.on_update then
self:on_update( dt )
end
for i = #children, 1, -1 do
children[i]:update( dt )
end
end
function Container:draw( surface, x, y )
local children = self.children
local cx, cy, cc
local x_offset, y_offset = self.x_offset, self.y_offset
self:reset_cursor_blink()
surface:fillRect( x, y, self.width, self.height, self.colour )
if self.on_pre_draw then
self:on_pre_draw()
end
for i = 1, #children do
local child = children[i]
if child:is_visible() then
child:draw( surface, x + child.x + x_offset, y + child.y + y_offset )
if child.cursor_active then
cx, cy, cc = child.x + child.cursor_x, child.y + child.cursor_y, child.cursor_colour
end
end
end
if cx then
self:set_cursor_blink( cx, cy, cc )
end
if self.on_post_draw then
self:on_post_draw()
end
self.changed = false
end
function Container:handle( event )
local children = self:get_children()
local x_offset, y_offset = self.x_offset, self.y_offset
if event:type_of( MouseEvent ) then
local within = event:is_within_area( 0, 0, self.width, self.height )
for i = #children, 1, -1 do
children[i]:handle( event:clone( children[i].x + x_offset, children[i].y + y_offset, within ) )
end
else
for i = #children, 1, -1 do
children[i]:handle( event )
end
end
if event:type_of( MouseEvent ) then
if event:is( 6 ) and event:is_within_area( 0, 0, self.width, self.height ) and event.within then
event.button[#event.button + 1] = self
end
self:on_mouse_event( event )
elseif event:type_of( KeyboardEvent ) and self.handles_keyboard and self.on_keyboard_event then
self:on_keyboard_event( event )
elseif event:type_of( TextEvent ) and self.handles_text and self.on_text_event then
self:on_text_event( event )
end
end
local wrapline, wrap
IHasText = class.new_interface( "IHasText", nil ) {
text = "";
text_lines = nil;
horizontal_alignment = 0;
vertical_alignment = 3;
text_colour = 1;
}
function IHasText:IHasText()
local function wrap()
self:wrap_text()
self:set_changed()
end
self.values:add( "text", "" )
self.values:add( "text_colour", 1 )
self.values:add( "horizontal_alignment", 0 )
self.values:add( "vertical_alignment", 3 )
self.values:subscribe( "width", {}, wrap )
self.values:subscribe( "text", {}, wrap )
end
function IHasText:auto_height()
if not self.text_lines then
self:wrap_text( true )
end
return self:set_height( #self.text_lines )
end
function IHasText:wrap_text( ignore_height )
self.text_lines = wrap( self.text, self.width, not ignore_height and self.height )
end
function IHasText:draw_text( surface, x, y )
local offset, lines = 0, self.text_lines
local horizontal_alignment = self.horizontal_alignment
local vertical_alignment = self.vertical_alignment
if not lines then
self:wrap_text()
lines = self.text_lines
end
if vertical_alignment == 1 then
offset = math.floor( self.height / 2 - #lines / 2 + .5 )
elseif vertical_alignment == 4 then
offset = self.height - #lines
end
for i = 1, #lines do
local x_offset = 0
if horizontal_alignment == 1 then
x_offset = math.floor( self.width / 2 - #lines[i] / 2 + .5 )
elseif horizontal_alignment == 2 then
x_offset = self.width - #lines[i]
end
surface:drawString( x + x_offset, y + offset + i - 1, lines[i], nil, self.text_colour )
end
end
function IHasText:on_pre_draw()
self:draw_text "default"
end
function wrapline( text, width )
if text:sub( 1, width ):find "\n" then
return text:match "^(.-)\n[^%S\n]*(.*)$"
end
if #text <= width then
return text
end
for i = width + 1, 1, -1 do
if text:sub( i, i ):find "%s" then
return text:sub( 1, i - 1 ):gsub( "[^%S\n]+$", "" ), text:sub( i + 1 ):gsub( "^[^%S\n]+", "" )
end
end
return text:sub( 1, width ), text:sub( width + 1 )
end
function wrap( text, width, height )
local lines, line = {}
while text and ( not height or #lines < height ) do
line, text = wrapline( text, width )
lines[#lines + 1] = line
end
return lines
end
Button = class.new( "Button", Sheet, IHasText, IColoured ) {
down = false;
colour = nil;
active_colour = nil;
horizontal_alignment = 1;
vertical_alignment = 1;
}
function Button:Button( x, y, width, height, text )
self:initialise()
self:IHasText()
self:IColoured()
self.values:add( "active_colour", 8 )
self:Sheet( x, y, width, height )
self:set_colour( 512 )
self:set_horizontal_alignment( 1 )
self:set_vertical_alignment( 1 )
if text then
self:set_text( text )
end
end
function Button:draw( surface, x, y )
surface:fillRect( x, y, self.width, self.height, self.down and self.active_colour or self.colour, 1, "" )
self:draw_text( surface, x, y )
self.changed = false
end
function Button:on_mouse_event( event )
if event:is( 1 ) and self.down then
self.down = false
self:set_changed()
end
if event.handled or not event:is_within_area( 0, 0, self.width, self.height ) or not event.within then
return
end
if event:is( 0 ) and not self.down then
self.down = true
self:set_changed()
event:handle()
elseif event:is( 2 ) then
if self.on_click then
self:on_click( event.button, event.x, event.y )
end
event:handle()
elseif event:is( 3 ) then
if self.on_hold then
self:on_hold( event.button, event.x, event.y )
end
event:handle()
end
end
-- @/require elements.Checkbox
ClippedContainer = class.new( "ClippedContainer", Container, nil ) {
surface = nil;
colour = nil;
}
function ClippedContainer:ClippedContainer( ... )
self.surface = surface.create( 0, 0 )
return self:Container( ... )
end
function ClippedContainer:draw( surface, x, y )
if self.changed then
local children = self.children
local cx, cy, cc
local x_offset, y_offset = self.x_offset, self.y_offset
self:reset_cursor_blink()
self.surface:clear( self.colour )
if self.on_pre_draw then
self:on_pre_draw()
end
for i = 1, #children do
local child = children[i]
if child:is_visible() then
child:draw( self.surface, child.x + x_offset, child.y + y_offset )
if child.cursor_active then
cx, cy, cc = child.x + child.cursor_x, child.y + child.cursor_y, child.cursor_colour
end
end
end
if cx then
self:set_cursor_blink( cx, cy, cc )
end
if self.on_post_draw then
self:on_post_draw()
end
self.changed = false
end
surface:drawSurface( self.surface, x, y )
end
-- @/require interfaces.IHasText
-- @/require elements.Draggable
-- @/require elements.Image
KeyHandler = class.new( "KeyHandler", Sheet, nil ) {
actions = {};
shortcuts = {};
handles_keyboard = true;
}
function KeyHandler:KeyHandler()
self.actions = {}
self.shortcuts = {}
self:initialise()
return self:Sheet( 0, 0, 0, 0 )
end
function KeyHandler:add_action( name, callback, ... )
for i = 1, #self.actions do
if self.actions[i].name == name then
Exception.throw( Exception( "KeyHandlerAction", "cannot create new action'" .. name .. "':action already exists" ) ) -- TODO: create custom exception for this
end
end
self.actions[#self.actions + 1] = {
name = name;
callback = callback;
parameters = { ... };
keybindings = {};
}
end
function KeyHandler:remove_action( name )
for i = 1, #self.actions do
if self.actions[i].name == name then
for j = 1, #self.actions[i].keybindings do
self:unbind_key( self.actions[i].keybindings[j] )
end
return table.remove( self.actions, i ).callback
end
end
end
function KeyHandler:set_callback( action, callback )
for i = 1, #self.actions do
if self.actions[i].name == action then
self.actions[i].callback = callback
return
end
end
end
function KeyHandler:set_parameters( action, parameters )
for i = 1, #self.actions do
if self.actions[i].name == action then
self.actions[i].parameters = parameters
return
end
end
end
function KeyHandler:bind_key( key, action )
if self.shortcuts[key] then
self:unbind_key( key )
end
for i = 1, #self.actions do
if self.actions[i].name == action then
self.actions[i].keybindings[#self.actions[i].keybindings + 1] = key
self.shortcuts[key] = action
return
end
end
Exception.throw( Exception( "KeyHandlerBindingException", "cannot bind key'" .. key .. "'to action'" .. action .. "':action doesn't exist" ) )
end
function KeyHandler:unbind_key( key )
local action = self.shortcuts[key]
if not action then
Exception.throw( Exception( "KeyHandlerBindingException", "cannot unbind key'" .. key .. "':key not bound" ) )
end
for i = 1, #self.actions do
if self.actions[i].name == action then
for j = 1, #self.actions[i].keybindings do
if self.actions[i].keybindings[j] == key then
table.remove( self.actions[i].keybindings, j )
break
end
end
self.shortcuts[key] = nil
return
end
end
end
function KeyHandler:on_keyboard_event( event )
if not event.handled and event:is( 7 ) then
local longest_match, longest_match_action
local actions = self.actions
local shortcuts = self.shortcuts
local k, v = next( shortcuts )
while k do
if event:matches( k ) then
if not longest_match or #k > #longest_match then
longest_match = k
longest_match_action = v
end
end
k, v = next( shortcuts, k )
end
if longest_match then
event:handle( self )
for i = 1, #actions do
if actions[i].name == longest_match_action then
return actions[i].callback( unpack( actions[i].parameters ) )
end
end
end
end
end
function KeyHandler:draw() end
Panel = class.new( "Panel", Sheet, IColoured ) {
colour = nil;
}
function Panel:Panel( x, y, w, h )
self:initialise()
self:IColoured()
return self:Sheet( x, y, w, h )
end
function Panel:draw( canvas, x, y )
canvas:fillRect( x, y, self.width, self.height, self.colour, 1, " " )
end
-- @/require elements.ScrollContainer
-- @/require interfaces.IHasText
-- @/require elements.Text
-- @/require elements.TextInput
sheets.KeyHandler = KeyHandler;sheets.ICollatedChildren = ICollatedChildren;sheets.KeyboardEvent = KeyboardEvent;sheets.Exception = Exception;sheets.Event = Event;sheets.ITimer = ITimer;sheets.class = class;sheets.QueryTracker = QueryTracker;sheets.Stream = Stream;sheets.Transition = Transition;sheets.Application = Application;sheets.IQueryable = IQueryable;sheets.ThreadRuntimeException = ThreadRuntimeException;sheets.Button = Button;sheets.MouseEvent = MouseEvent;sheets.ClippedContainer = ClippedContainer;sheets.ISize = ISize;sheets.TableType = TableType;sheets.Easing = Easing;sheets.MiscEvent = MiscEvent;sheets.IColoured = IColoured;sheets.Thread = Thread;sheets.Container = Container;sheets.TextEvent = TextEvent;sheets.parameters = parameters;sheets.Panel = Panel;sheets.ResourceLoadException = ResourceLoadException;sheets.Codegen = Codegen;sheets.Typechecking = Typechecking;sheets.UnionType = UnionType;sheets.ValueHandler = ValueHandler;sheets.IncorrectParameterException = IncorrectParameterException;sheets.DynamicValueParser = DynamicValueParser;sheets.Sheet = Sheet;sheets.IHasText = IHasText;sheets.Screen = Screen;sheets.Type = Type;sheets.IChildContainer = IChildContainer;sheets.IncorrectConstructorException = IncorrectConstructorException;sheets.ListType = ListType;sheets.clipboard = clipboard;sheets.ITagged = ITagged end
local app = sheets.Application()
app:run()
end )
if not ok then
local e = select( 2, pcall( error, "@", 2 ) )
local src = e:match "^(.*):%d+: @$"
local line, msg = err:match( src .. ":(%d+): (.*)" )
if line then
local src, line = __get_src_and_line( tonumber( line ) )
error( src .. "[" .. line .. "]: " .. __get_err_msg( src, line, msg ), 0 )
else
error( err, 0 )
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment