Created
February 19, 2017 21:01
-
-
Save exerro/7ef25646c18b137b31796f32d33997d2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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