opus/sys/modules/opus/ui/canvas.lua

414 lines
9.3 KiB
Lua

local class = require('opus.class')
local Region = require('opus.ui.region')
local Util = require('opus.util')
local _rep = string.rep
local _sub = string.sub
local _gsub = string.gsub
local colors = _G.colors
local Canvas = class()
local function genPalette(map)
local t = { }
local rcolors = Util.transpose(colors)
for n = 1, 16 do
local pow = 2 ^ (n - 1)
local ch = _sub(map, n, n)
t[pow] = ch
t[rcolors[pow]] = ch
end
return t
end
Canvas.colorPalette = genPalette('0123456789abcdef')
Canvas.grayscalePalette = genPalette('088888878877787f')
--[[
A canvas can have more lines than canvas.height in order to scroll
TODO: finish vertical scrolling
]]
function Canvas:init(args)
self.bg = colors.black
self.fg = colors.white
Util.merge(self, args)
self.x = self.x or 1
self.y = self.y or 1
self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1
if not self.palette then
if self.isColor then
self.palette = Canvas.colorPalette
else
self.palette = Canvas.grayscalePalette
end
end
self.lines = { }
for i = 1, self.height do
self.lines[i] = { }
end
self:clear()
end
function Canvas:move(x, y)
self.x, self.y = x, y
self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1
if self.parent then
self.parent:dirty(true)
end
end
function Canvas:resize(w, h)
self:resizeBuffer(w, h)
self.ex = self.x + w - 1
self.ey = self.y + h - 1
self.width = w
self.height = h
end
-- resize the canvas buffer - not the canvas itself
function Canvas:resizeBuffer(w, h)
for i = #self.lines + 1, h do
self.lines[i] = { }
self:clearLine(i)
end
while #self.lines > h do
table.remove(self.lines, #self.lines)
end
if w < self.width then
for i = 1, h do
local ln = self.lines[i]
ln.text = _sub(ln.text, 1, w)
ln.fg = _sub(ln.fg, 1, w)
ln.bg = _sub(ln.bg, 1, w)
end
elseif w > self.width then
local d = w - self.width
local text = _rep(' ', d)
local fg = _rep(self.palette[self.fg], d)
local bg = _rep(self.palette[self.bg], d)
for i = 1, h do
local ln = self.lines[i]
ln.text = ln.text .. text
ln.fg = ln.fg .. fg
ln.bg = ln.bg .. bg
ln.dirty = true
end
end
end
function Canvas:copy()
local b = Canvas({
x = self.x,
y = self.y,
width = self.width,
height = self.height,
isColor = self.isColor,
})
for i = 1, #self.lines do
b.lines[i].text = self.lines[i].text
b.lines[i].fg = self.lines[i].fg
b.lines[i].bg = self.lines[i].bg
end
return b
end
function Canvas:addLayer(layer)
layer.parent = self
if not self.children then
self.children = { }
end
table.insert(self.children, 1, layer)
return layer
end
function Canvas:removeLayer()
for k, layer in pairs(self.parent.children) do
if layer == self then
self:setVisible(false)
table.remove(self.parent.children, k)
break
end
end
end
function Canvas:setVisible(visible)
self.visible = visible -- TODO: use self.active = visible
if not visible and self.parent then
self.parent:dirty()
-- TODO: set parent's lines to dirty for each line in self
end
end
-- Push a layer to the top
function Canvas:raise()
if self.parent and self.parent.children then
for k, v in pairs(self.parent.children) do
if v == self then
table.insert(self.parent.children, table.remove(self.parent.children, k))
break
end
end
end
end
function Canvas:write(x, y, text, bg, fg)
if bg then
bg = _rep(self.palette[bg], #text)
end
if fg then
fg = _rep(self.palette[fg] or self.palette[1], #text)
end
self:blit(x, y, text, bg, fg)
end
function Canvas:blit(x, y, text, bg, fg)
if y > 0 and y <= #self.lines and x <= self.width then
local width = #text
local tx, tex
if x < 1 then
tx = 2 - x
width = width + x - 1
x = 1
end
if x + width - 1 > self.width then
tex = self.width - x + (tx or 1)
width = tex - (tx or 1) + 1
end
if width > 0 then
local function replace(sstr, rstr)
if tx or tex then
rstr = _sub(rstr, tx or 1, tex)
end
if x == 1 and width == self.width then
return rstr
elseif x == 1 then
return rstr .. _sub(sstr, x + width)
elseif x + width > self.width then
return _sub(sstr, 1, x - 1) .. rstr
end
return _sub(sstr, 1, x - 1) .. rstr .. _sub(sstr, x + width)
end
local line = self.lines[y]
line.dirty = true
line.text = replace(line.text, text)
if fg then
line.fg = replace(line.fg, fg)
end
if bg then
line.bg = replace(line.bg, bg)
end
end
end
end
function Canvas:writeLine(y, text, fg, bg)
if y > 0 and y <= #self.lines then
self.lines[y].dirty = true
self.lines[y].text = text
self.lines[y].fg = fg
self.lines[y].bg = bg
end
end
function Canvas:clearLine(y, bg, fg)
fg = _rep(self.palette[fg or self.fg], self.width)
bg = _rep(self.palette[bg or self.bg], self.width)
self:writeLine(y, _rep(' ', self.width), fg, bg)
end
function Canvas:clear(bg, fg)
local text = _rep(' ', self.width)
fg = _rep(self.palette[fg or self.fg], self.width)
bg = _rep(self.palette[bg or self.bg], self.width)
for i = 1, #self.lines do
self:writeLine(i, text, fg, bg)
end
end
function Canvas:isDirty()
for i = 1, #self.lines do
if self.lines[i].dirty then
return true
end
end
end
function Canvas:dirty(includingChildren)
if self.lines then
for i = 1, #self.lines do
self.lines[i].dirty = true
end
if includingChildren and self.children then
for _, child in pairs(self.children) do
child:dirty(true)
end
end
end
end
function Canvas:clean()
for i = 1, #self.lines do
self.lines[i].dirty = nil
end
end
function Canvas:applyPalette(palette)
local lookup = { }
for n = 1, 16 do
lookup[self.palette[2 ^ (n - 1)]] = palette[2 ^ (n - 1)]
end
for _, l in pairs(self.lines) do
l.fg = _gsub(l.fg, '%w', lookup)
l.bg = _gsub(l.bg, '%w', lookup)
l.dirty = true
end
self.palette = palette
end
-- either render directly to the device
-- or use another canvas as a backing buffer
function Canvas:render(device, doubleBuffer)
self.regions = Region.new(self.x, self.y, self.ex, self.ey)
self:__renderLayers(device, { x = self.x - 1, y = self.y - 1 }, doubleBuffer)
-- doubleBuffering to reduce the amount of
-- setCursorPos, blits
if doubleBuffer then
--[[
local drew = false
local bg = _rep(2, device.width)
for k,v in pairs(device.lines) do
if v.dirty then
device.device.setCursorPos(device.x, device.y + k - 1)
device.device.blit(v.text, v.fg, bg)
drew = true
end
end
if drew then
local c = os.clock()
repeat until os.clock()-c > .1
end
]]
for k,v in pairs(device.lines) do
if v.dirty then
device.device.setCursorPos(device.x, device.y + k - 1)
device.device.blit(v.text, v.fg, v.bg)
v.dirty = false
end
end
end
end
-- regions are comprised of absolute values that correspond to the output device.
-- canvases have coordinates relative to their parent.
-- canvas layer's stacking order is determined by the position within the array.
-- layers in the beginning of the array are overlayed by layers further down in
-- the array.
function Canvas:__renderLayers(device, offset, doubleBuffer)
if self.children then
for i = #self.children, 1, -1 do
local canvas = self.children[i]
if canvas.visible or canvas.enabled then
-- get the area to render for this layer
canvas.regions = Region.new(
canvas.x + offset.x - (self.offx or 0),
canvas.y + offset.y - (self.offy or 0),
canvas.ex + offset.x - (self.offx or 0),
canvas.ey + offset.y - (self.offy or 0))
-- contain within parent
canvas.regions:andRegion(self.regions)
-- punch out this area from the parent's canvas
self.regions:subRect(
canvas.x + offset.x - (self.offx or 0),
canvas.y + offset.y - (self.offy or 0),
canvas.ex + offset.x - (self.offx or 0),
canvas.ey + offset.y - (self.offy or 0))
if #canvas.regions.region > 0 then
canvas:__renderLayers(device, {
x = canvas.x + offset.x - 1 - (self.offx or 0),
y = canvas.y + offset.y - 1 - (self.offy or 0),
}, doubleBuffer)
end
canvas.regions = nil
end
end
end
for _,region in ipairs(self.regions.region) do
self:__blitRect(device,
{ x = region[1] - offset.x,
y = region[2] - offset.y,
ex = region[3] - offset.x,
ey = region[4] - offset.y },
{ x = region[1], y = region[2] },
doubleBuffer)
end
self.regions = nil
self:clean()
end
function Canvas:__blitRect(device, src, tgt, doubleBuffer)
-- for visualizing updates on the screen
--[[
if Canvas.__visualize or self.visualize then
local drew
local t = _rep(' ', src.ex-src.x + 1)
local bg = _rep(2, src.ex-src.x + 1)
for i = 0, src.ey - src.y do
local line = self.lines[src.y + i + (self.offy or 0)]
if line and line.dirty then
drew = true
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, bg, bg)
end
end
if drew then
local c = os.clock()
repeat until os.clock()-c > .03
end
end
]]
for i = 0, src.ey - src.y do
local line = self.lines[src.y + i + (self.offy or 0)]
if line and line.dirty then
local t, fg, bg = line.text, line.fg, line.bg
if src.x > 1 or src.ex < self.ex then
t = _sub(t, src.x, src.ex)
fg = _sub(fg, src.x, src.ex)
bg = _sub(bg, src.x, src.ex)
end
if doubleBuffer then
Canvas.blit(device, tgt.x, tgt.y + i,
t, bg, fg)
else
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, fg, bg)
end
end
end
end
return Canvas