ldd-CC/windont/windont.lua

668 lines
21 KiB
Lua

-- Windon't
-- enhanced window API by LDDestroier
-- intended for general use within all me new programs
--
-- Unique features:
-- + Transparency within windows
-- + Built-in window layering
-- stores each base terminal's framebuffers to optimize rendering
local oldScreenBuffer = {}
local table_insert = table.insert
local table_concat = table.concat
local math_floor = math.floor
local to_blit = {}
local to_colors = {}
local table_compare = function(tbl1, tbl2)
if type(tbl1) ~= "table" or type(tbl2) ~= "table" then
return tbl1 == tbl2
else
for k,v in pairs(tbl1) do
if tbl1[k] ~= tbl2[k] then
return false
end
end
for k,v in pairs(tbl2) do
if tbl1[k] ~= tbl2[k] then
return false
end
end
return true
end
end
local getTime = function()
return 24 * os.day() + os.time()
end
for i = 1, 16 do
to_blit[2 ^ (i - 1)] = ("0123456789abcdef"):sub(i, i)
to_colors[("0123456789abcdef"):sub(i, i)] = 2 ^ (i - 1)
end
to_blit[0], to_colors["-"] = "-", 0
local nativePalette = { -- native palette colors, since some terminals are naughty and don't contain term.nativePaletteColor()
[ 1 ] = {
0.94117647409439,
0.94117647409439,
0.94117647409439,
},
[ 2 ] = {
0.94901961088181,
0.69803923368454,
0.20000000298023,
},
[ 4 ] = {
0.89803922176361,
0.49803921580315,
0.84705883264542,
},
[ 8 ] = {
0.60000002384186,
0.69803923368454,
0.94901961088181,
},
[ 16 ] = {
0.87058824300766,
0.87058824300766,
0.42352941632271,
},
[ 32 ] = {
0.49803921580315,
0.80000001192093,
0.098039217293262,
},
[ 64 ] = {
0.94901961088181,
0.69803923368454,
0.80000001192093,
},
[ 128 ] = {
0.29803922772408,
0.29803922772408,
0.29803922772408,
},
[ 256 ] = {
0.60000002384186,
0.60000002384186,
0.60000002384186,
},
[ 512 ] = {
0.29803922772408,
0.60000002384186,
0.69803923368454,
},
[ 1024 ] = {
0.69803923368454,
0.40000000596046,
0.89803922176361,
},
[ 2048 ] = {
0.20000000298023,
0.40000000596046,
0.80000001192093,
},
[ 4096 ] = {
0.49803921580315,
0.40000000596046,
0.29803922772408,
},
[ 8192 ] = {
0.34117648005486,
0.65098041296005,
0.30588236451149,
},
[ 16384 ] = {
0.80000001192093,
0.29803922772408,
0.29803922772408,
},
[ 32768 ] = {
0.066666670143604,
0.066666670143604,
0.066666670143604,
}
}
-- list of all completely blank characters
local whitespace = {
["\9"] = true,
["\10"] = true,
["\13"] = true,
["\32"] = true,
["\128"] = true
}
-- check if space on screenBuffer is transparent
local checkTransparent = function(buffer, x, y, blitLayer)
if buffer[blitLayer or 1][y] then
if blitLayer then
return (buffer[blitLayer][y][x] and buffer[blitLayer][y][x] ~= "-")
else
if (not buffer[2][y][x] or buffer[2][y][x] == "-") and (not buffer[3][y][x] or buffer[3][y][x] == "-") then
return false
elseif (not buffer[3][y][x] or buffer[3][y][x] == "-") and (not buffer[1][y][x] or whitespace[buffer[1][y][x]]) then
return false
else
return buffer[1][y][x] and buffer[2][y][x] and buffer[3][y][x]
end
end
end
end
local expect = function(value, default, valueType)
if value == nil or (valueType and type(value) ~= valueType) then
return default
else
return value
end
end
local windont = {
doClearScreen = false, -- if true, will clear the screen during render
ignoreUnchangedLines = true, -- if true, the render function will check each line it renders against the last framebuffer and ignore it if they are the same
useSetVisible = false, -- if true, sets the base terminal's visibility to false before rendering
sameCharWillStencil = false, -- if true, if one window is layered atop another and both windows have a spot where the character is the same, and the top window's text color is transparent, it will use the TEXT color of the lower window instead of the BACKGROUND color
default = {
baseTerm = term.current(), -- default base terminal for all windows
textColor = "0", -- default text color (what " " corresponds to in term.blit's second argument)
backColor = "f", -- default background color (what " " corresponds to in term.blit's third argument)
blink = true,
visible = true,
alwaysRender = true, -- if true, new windows will always render if they are written to
},
info = {
BLIT_CALLS = 0, -- amount of term.blit calls during the last render
LAST_RENDER_TIME = 0, -- last time in which render was called
LAST_RENDER_AMOUNT = 0, -- amount of windows drawn during last render
LAST_RENDER_WINDOWS = {}, -- table of the last window objects that were rendered
}
}
-- draws one or more windon't objects
-- should not draw over any terminal space that isn't occupied by a window
windont.render = function(options, ...)
-- potential options:
-- number: onlyX1
-- number: onlyX2
-- number: onlyY
-- boolean: force (forces render / ignores optimiztaion that compares current framebuffer to old one)
-- terminal: baseTerm (forces to render onto this terminal instead of the window's base terminal)
local windows = {...}
options = options or {}
local bT, scr_x, scr_y
-- checks if "options" is actually the first window, just in case
if type(options.meta) == "table" then
if (
type(options.meta.buffer) == "table" and
type(options.meta.x) == "number" and
type(options.meta.y) == "number" and
type(options.meta.newBuffer) == "function"
) then
table_insert(windows, 1, options)
end
end
local screenBuffer = {{}, {}, {}}
local blitList = {} -- list of blit commands per line
local c = 1 -- current blitList entry
local cTime = getTime()
local AMNT_OF_BLITS = 0 -- how many blit calls are there?
local cx, cy -- each window's absolute X and Y
local char_cx, text_cx, back_cx -- each window's transformed absolute X's in table form
local char_cy, text_cy, back_cy -- each window's transformed absolute X's in table form
local buffer -- each window's buffer
local newChar, newText, newBack -- if the transformation function declares a new dot, this is it
local oriChar, oriText, oriBack
local char_out, text_out, back_out -- three tables, directly returned from the transformation functions
local baseTerms = {}
if type(options.baseTerm) == "table" then
for i = 1, #windows do
baseTerms[options.baseTerm] = baseTerms[options.baseTerm] or {}
baseTerms[options.baseTerm][i] = true
end
else
for i = 1, #windows do
baseTerms[windows[i].meta.baseTerm] = baseTerms[windows[i].meta.baseTerm] or {}
baseTerms[windows[i].meta.baseTerm][i] = true
end
end
for bT, bT_list in pairs(baseTerms) do
if bT == output then
bT = options.baseTerm or output.meta.baseTerm
end
if windont.useSetVisible and bT.setVisible then
bT.setVisible(false)
end
scr_x, scr_y = bT.getSize()
-- try entire buffer transformations
for i = #windows, 1, -1 do
if bT_list[i] then
if windows[i].meta.metaTransformation then
-- metaTransformation functions needn't return a value
windows[i].meta.metaTransformation(windows[i].meta)
end
end
end
for y = options.onlyY or 1, options.onlyY or scr_y do
screenBuffer[1][y] = {}
screenBuffer[2][y] = {}
screenBuffer[3][y] = {}
blitList = {}
c = 1
for x = options.onlyX1 or 1, math.min(scr_x, options.onlyX2 or scr_x) do
for i = #windows, 1, -1 do
if bT_list[i] then
newChar, newText, newBack = nil
if windows[i].meta.visible then
buffer = windows[i].meta.buffer
cx = 1 + x + -windows[i].meta.x
cy = 1 + y + -windows[i].meta.y
char_cx, text_cx, back_cx = cx, cx, cx
char_cy, text_cy, back_cy = cy, cy, cy
oriChar = (buffer[1][cy] or {})[cx]
oriText = (buffer[2][cy] or {})[cx]
oriBack = (buffer[3][cy] or {})[cx]
-- try transformation
if windows[i].meta.transformation then
char_out, text_out, back_out = windows[i].meta.transformation(cx, cy, oriChar, oriText, oriBack, windows[i].meta)
if char_out then
char_cx = math_floor(char_out[1] or cx)
char_cy = math_floor(char_out[2] or cy)
if (char_out[1] % 1 ~= 0) or (char_out[2] % 1 ~= 0) then
newChar = " "
else
newChar = char_out[3]
end
end
if text_out then
text_cx = math_floor(text_out[1] or cx)
text_cy = math_floor(text_out[2] or cy)
newText = text_out[3]
end
if back_out then
back_cx = math_floor(back_out[1] or cx)
back_cy = math_floor(back_out[2] or cy)
newBack = back_out[3]
end
end
if checkTransparent(buffer, char_cx, char_cy) or checkTransparent(buffer, text_cx, text_cy) or checkTransparent(buffer, back_cx, back_cy) then
screenBuffer[2][y][x] = newText or checkTransparent(buffer, text_cx, text_cy, 2) and (buffer[2][text_cy][text_cx]) or (
(buffer[1][text_cy][text_cx] == screenBuffer[1][y][x]) and (windont.sameCharWillStencil) and
screenBuffer[2][y][x]
or
screenBuffer[3][y][x]
)
screenBuffer[1][y][x] = newChar or checkTransparent(buffer, char_cx, char_cy ) and (buffer[1][char_cy][char_cx]) or screenBuffer[1][y][x]
screenBuffer[3][y][x] = newBack or checkTransparent(buffer, back_cx, back_cy, 3) and (buffer[3][back_cy][back_cx]) or screenBuffer[3][y][x]
end
end
end
end
if windont.doClearScreen then
screenBuffer[1][y][x] = screenBuffer[1][y][x] or " "
end
screenBuffer[2][y][x] = screenBuffer[2][y][x] or windont.default.backColor -- intentionally not the default text color
screenBuffer[3][y][x] = screenBuffer[3][y][x] or windont.default.backColor
if checkTransparent(screenBuffer, x, y) then
if checkTransparent(screenBuffer, -1 + x, y) then
blitList[c][1] = blitList[c][1] .. screenBuffer[1][y][x]
blitList[c][2] = blitList[c][2] .. screenBuffer[2][y][x]
blitList[c][3] = blitList[c][3] .. screenBuffer[3][y][x]
else
c = x
blitList[c] = {
screenBuffer[1][y][x],
screenBuffer[2][y][x],
screenBuffer[3][y][x]
}
end
end
end
if (not oldScreenBuffer[bT]) or (not windont.ignoreUnchangedLines) or (options.force) or (
(not table_compare(screenBuffer[1][y], oldScreenBuffer[bT][1][y])) or
(not table_compare(screenBuffer[2][y], oldScreenBuffer[bT][2][y])) or
(not table_compare(screenBuffer[3][y], oldScreenBuffer[bT][3][y]))
) then
for k,v in pairs(blitList) do
bT.setCursorPos(k, y)
bT.blit(v[1], v[2], v[3])
AMNT_OF_BLITS = 1 + AMNT_OF_BLITS
end
end
end
oldScreenBuffer[bT] = screenBuffer
if windont.useSetVisible and bT.setVisible then
if not multishell then
bT.setVisible(true)
elseif multishell.getFocus() == multishell.getCurrent() then
bT.setVisible(true)
end
end
end
windont.info.LAST_RENDER_AMOUNT = #windows
windont.info.BLIT_CALLS = AMNT_OF_BLITS
windont.info.LAST_RENDER_WINDOWS = windows
windont.info.LAST_RENDER_TIME = cTime
windont.info.LAST_RENDER_DURATION = getTime() + -cTime
end
-- creates a new windon't object that can be manipulated the same as a regular window
windont.newWindow = function( x, y, width, height, misc )
-- check argument types
assert(type(x) == "number", "argument #1 must be number, got " .. type(x))
assert(type(y) == "number", "argument #2 must be number, got " .. type(y))
assert(type(width) == "number", "argument #3 must be number, got " .. type(width))
assert(type(height) == "number", "argument #4 must be number, got " .. type(height))
-- check argument validity
assert(x > 0, "x position must be above zero")
assert(y > 0, "y position must be above zero")
assert(width > 0, "width must be above zero")
assert(height > 0, "height must be above zero")
local output = {}
misc = misc or {}
local meta = {
x = expect(x, 1), -- x position of the window
y = expect(y, 1), -- y position of the window
width = width, -- width of the buffer
height = height, -- height of the buffer
buffer = expect(misc.buffer, {}, "table"), -- stores contents of terminal in buffer[1][y][x] format
renderBuddies = expect(misc.renderBuddies, {}, "table"), -- renders any other window objects stored here after rendering here
baseTerm = expect(misc.baseTerm, windont.default.baseTerm, "table"), -- base terminal for which this window draws on
isColor = expect(misc.isColor, term.isColor(), "boolean"), -- if true, then it's an advanced computer
transformation = expect(misc.transformation, nil, "function"), -- function that transforms the char/text/back dots of the window
metaTransformation = expect(misc.metaTransformation, nil, "function"), -- function that transforms the whole output.meta function
cursorX = expect(misc.cursorX, 1),
cursorY = expect(misc.cursorY, 1),
textColor = expect(misc.textColor, windont.default.textColor, "string"), -- current text color
backColor = expect(misc.backColor, windont.default.backColor, "string"), -- current background color
blink = expect(misc.blink, windont.default.blink, "boolean"), -- cursor blink
alwaysRender = expect(misc.alwaysRender, windont.default.alwaysRender, "boolean"), -- render after every terminal operation
visible = expect(misc.visible, windont.default.visible, "boolean"), -- if false, don't render ever
-- make a new buffer (optionally uses an existing buffer as a reference)
newBuffer = function(width, height, char, text, back, drawAtop)
local output = {{}, {}, {}}
drawAtop = drawAtop or {{}, {}, {}}
for y = 1, height do
output[1][y] = output[1][y] or {}
output[2][y] = output[2][y] or {}
output[3][y] = output[3][y] or {}
for x = 1, width do
output[1][y][x] = (drawAtop[1][y] or {})[x] or (output[1][y][x] or (char or " "))
output[2][y][x] = (drawAtop[2][y] or {})[x] or (output[2][y][x] or (text or "0"))
output[3][y][x] = (drawAtop[3][y] or {})[x] or (output[3][y][x] or (back or "f"))
end
end
return output
end
}
bT = meta.baseTerm
-- initialize the buffer
meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor)
output.meta = meta
output.write = function(text)
assert(type(text) == "string" or type(text) == "number", "expected string, got " .. type(text))
local initX = meta.cursorX
for i = 1, #tostring(text) do
if meta.cursorX >= 1 and meta.cursorX <= meta.width and meta.cursorY >= 1 and meta.cursorY <= meta.height then
if not meta.buffer[1] then
error("what the fuck happened")
end
meta.buffer[1][meta.cursorY][meta.cursorX] = tostring(text):sub(i,i)
meta.buffer[2][meta.cursorY][meta.cursorX] = meta.textColor
meta.buffer[3][meta.cursorY][meta.cursorX] = meta.backColor
end
meta.cursorX = meta.cursorX + 1
end
if meta.alwaysRender then
output.redraw(
-1 + meta.x + initX,
-1 + meta.x + meta.cursorX,
-1 + meta.y + meta.cursorY
)
end
end
output.blit = function(char, text, back)
assert(type(char) == "string" and type(text) == "string" and type(back) == "string", "all arguments must be strings")
assert(#char == #text and #text == #back, "arguments must be same length")
local initX = meta.cursorX
for i = 1, #char do
if meta.cursorX >= 1 and meta.cursorX <= meta.width and meta.cursorY >= 1 and meta.cursorY <= meta.height then
meta.buffer[1][meta.cursorY][meta.cursorX] = char:sub(i,i)
meta.buffer[2][meta.cursorY][meta.cursorX] = to_colors[text:sub(i,i)] and windont.default.textColor or text:sub(i,i)
meta.buffer[3][meta.cursorY][meta.cursorX] = to_colors[back:sub(i,i)] and windont.default.backColor or back:sub(i,i)
meta.cursorX = meta.cursorX + 1
end
end
if meta.alwaysRender then
output.redraw(
-1 + meta.x + initX,
-1 + meta.x + meta.cursorX,
-1 + meta.y + meta.cursorY
)
end
end
output.setCursorPos = function(x, y)
assert(type(x) == "number", "argument #1 must be number, got " .. type(x))
assert(type(y) == "number", "argument #2 must be number, got " .. type(y))
meta.cursorX, meta.cursorY = math.floor(x), math.floor(y)
if meta.alwaysRender then
if bT == output then
bT = output.meta.baseTerm
end
bT.setCursorPos(
-1 + meta.x + meta.cursorX,
-1 + meta.y + meta.cursorY
)
end
end
output.getCursorPos = function()
return meta.cursorX, meta.cursorY
end
output.setTextColor = function(color)
if to_blit[color] then
meta.textColor = to_blit[color]
else
error("Invalid color (got " .. color .. ")")
end
end
output.setTextColour = output.setTextColor
output.setBackgroundColor = function(color)
if to_blit[color] then
meta.backColor = to_blit[color]
else
error("Invalid color (got " .. color .. ")")
end
end
output.setBackgroundColour = output.setBackgroundColor
output.getTextColor = function()
return to_colors[meta.textColor]
end
output.getTextColour = output.getTextColor
output.getBackgroundColor = function()
return to_colors[meta.backColor]
end
output.getBackgroundColour = output.getBackgroundColor
output.setVisible = function(visible)
assert(type(visible) == "boolean", "bad argument #1 (expected boolean, got " .. type(visible) .. ")")
meta.visible = visible and true or false
end
output.clear = function()
meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor)
if meta.alwaysRender then
output.redraw()
end
end
output.clearLine = function()
meta.buffer[1][meta.cursorY] = nil
meta.buffer[2][meta.cursorY] = nil
meta.buffer[3][meta.cursorY] = nil
meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor, meta.buffer)
if meta.alwaysRender then
bT.setCursorPos(meta.x, -1 + meta.y + meta.cursorY)
bT.blit(
(" "):rep(meta.width),
(meta.textColor):rep(meta.width),
(meta.backColor):rep(meta.width)
)
end
end
output.getLine = function(y)
assert(type(y) == "number", "bad argument #1 (expected number, got " .. type(y) .. ")")
assert(meta.buffer[1][y], "Line is out of range.")
return table_concat(meta.buffer[1][y]), table_concat(meta.buffer[2][y]), table_concat(meta.buffer[3][y])
end
output.scroll = function(amplitude)
if math.abs(amplitude) < meta.height then -- minor optimization
local blank = {{}, {}, {}}
for x = 1, meta.width do
blank[1][x] = " "
blank[2][x] = meta.textColor
blank[3][x] = meta.backColor
end
for y = 1, meta.height do
meta.buffer[1][y] = meta.buffer[1][y + amplitude] or blank[1]
meta.buffer[2][y] = meta.buffer[2][y + amplitude] or blank[2]
meta.buffer[3][y] = meta.buffer[3][y + amplitude] or blank[3]
end
else
meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor)
end
if meta.alwaysRender then
if math_floor(amplitude) ~= 0 then
output.redraw()
end
end
end
output.getSize = function()
return meta.width, meta.height
end
output.isColor = function()
return meta.isColor
end
output.isColour = output.isColor
output.reposition = function(x, y, width, height)
assert(type(x) == "number", "bad argument #1 (expected number, got " .. type(x) .. ")")
assert(type(y) == "number", "bad argument #2 (expected number, got " .. type(y) .. ")")
meta.x = math_floor(x)
meta.y = math_floor(y)
if width then
assert(type(width) == "number", "bad argument #3 (expected number, got " .. type(width) .. ")")
assert(type(height) == "number", "bad argument #4 (expected number, got " .. type(height) .. ")")
meta.width = width
meta.height = height
meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor, meta.buffer)
end
if meta.alwaysRender then
output.redraw()
end
end
output.restoreCursor = function()
bT.setCursorPos(
math.max(0, -1 + meta.x + meta.cursorX),
math.max(0, -1 + meta.y + meta.cursorY)
)
bT.setCursorBlink(meta.blink)
end
output.getPosition = function()
return meta.x, meta.y
end
output.setCursorBlink = function(blink)
meta.blink = blink and true or false
end
output.getCursorBlink = function(blink)
return meta.blink
end
output.setPaletteColor = bT.setPaletteColor
output.setPaletteColour = bT.setPaletteColour
output.getPaletteColor = bT.getPaletteColor
output.getPaletteColour = bT.getPaletteColour
if bT.getPaletteColor then
output.nativePaletteColor = bT.nativePaletteColor or function(col)
if nativePalette[col] then
return table.unpack(nativePalette[col])
else
return table.unpack(nativePalette[1]) -- I don't get how this function takes in non-base2 numbers...
end
end
end
output.redraw = function(x1, x2, y, options)
options = options or {}
options.onlyX1 = x1
options.onlyX2 = x2
options.onlyY = y
if #meta.renderBuddies > 0 then
windont.render(options, output, table.unpack(meta.renderBuddies))
else
windont.render(options, output)
end
output.restoreCursor()
end
if meta.alwaysRender then
output.redraw()
end
return output
end
return windont