mirror of
				https://github.com/kepler155c/opus
				synced 2025-10-31 15:43:00 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			499 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			499 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| local class  = require('class')
 | |
| local Region = require('ui.region')
 | |
| local Util   = require('util')
 | |
| 
 | |
| local _rep = string.rep
 | |
| local _sub = string.sub
 | |
| local _gsub = string.gsub
 | |
| local colors = _G.colors
 | |
| 
 | |
| local Canvas = class()
 | |
| 
 | |
| Canvas.colorPalette = { }
 | |
| Canvas.darkPalette = { }
 | |
| Canvas.grayscalePalette = { }
 | |
| 
 | |
| for n = 1, 16 do
 | |
| 	Canvas.colorPalette[2 ^ (n - 1)]     = _sub("0123456789abcdef", n, n)
 | |
| 	Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
 | |
| 	Canvas.darkPalette[2 ^ (n - 1)]      = _sub("8777777f77fff77f", n, n)
 | |
| end
 | |
| 
 | |
| function Canvas:init(args)
 | |
| 	self.x = 1
 | |
| 	self.y = 1
 | |
| 	self.layers = { }
 | |
| 
 | |
| 	Util.merge(self, args)
 | |
| 
 | |
| 	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
 | |
| 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
 | |
| end
 | |
| 
 | |
| function Canvas:resize(w, h)
 | |
| 	for i = self.height, h do
 | |
| 		self.lines[i] = { }
 | |
| 	end
 | |
| 
 | |
| 	while #self.lines > h do
 | |
| 		table.remove(self.lines, #self.lines)
 | |
| 	end
 | |
| 
 | |
| 	if w ~= self.width then
 | |
| 		for i = 1, self.height do
 | |
| 			self.lines[i] = { dirty = true }
 | |
| 		end
 | |
| 	end
 | |
| 
 | |
| 	self.ex = self.x + w - 1
 | |
| 	self.ey = self.y + h - 1
 | |
| 	self.width = w
 | |
| 	self.height = h
 | |
| 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.height 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)
 | |
| 	local canvas = Canvas({
 | |
| 		x       = layer.x,
 | |
| 		y       = layer.y,
 | |
| 		width   = layer.width,
 | |
| 		height  = layer.height,
 | |
| 		isColor = self.isColor,
 | |
| 	})
 | |
| 	canvas.parent = self
 | |
| 	table.insert(self.layers, canvas)
 | |
| 	return canvas
 | |
| end
 | |
| 
 | |
| function Canvas:removeLayer()
 | |
| 	for k, layer in pairs(self.parent.layers) do
 | |
| 		if layer == self then
 | |
| 			self:setVisible(false)
 | |
| 			table.remove(self.parent.layers, k)
 | |
| 			break
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Canvas:setVisible(visible)
 | |
| 	self.visible = visible
 | |
| 	if not visible then
 | |
| 		self.parent:dirty()
 | |
| 		-- set parent's lines to dirty for each line in self
 | |
| 	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], #text)
 | |
| 	end
 | |
| 	self:writeBlit(x, y, text, bg, fg)
 | |
| end
 | |
| 
 | |
| function Canvas:writeBlit(x, y, text, bg, fg)
 | |
| 	if y > 0 and y <= #self.lines and x <= self.width then
 | |
| 		local width = #text
 | |
| 
 | |
| 		-- fix ffs
 | |
| 		if x < 1 then
 | |
| 			text = _sub(text, 2 - x)
 | |
| 			if bg then
 | |
| 				bg = _sub(bg, 2 - x)
 | |
| 			end
 | |
| 			if bg then
 | |
| 				fg = _sub(fg, 2 - x)
 | |
| 			end
 | |
| 			width = width + x - 1
 | |
| 			x = 1
 | |
| 		end
 | |
| 
 | |
| 		if x + width - 1 > self.width then
 | |
| 			text = _sub(text, 1, self.width - x + 1)
 | |
| 			if bg then
 | |
| 				bg = _sub(bg, 1, self.width - x + 1)
 | |
| 			end
 | |
| 			if bg then
 | |
| 				fg = _sub(fg, 1, self.width - x + 1)
 | |
| 			end
 | |
| 			width = #text
 | |
| 		end
 | |
| 
 | |
| 		if width > 0 then
 | |
| 
 | |
| 			local function replace(sstr, pos, rstr, width)
 | |
| 				if pos == 1 and width == self.width then
 | |
| 					return rstr
 | |
| 				elseif pos == 1 then
 | |
| 					return rstr .. _sub(sstr, pos+width)
 | |
| 				elseif pos + width > self.width then
 | |
| 					return _sub(sstr, 1, pos-1) .. rstr
 | |
| 				end
 | |
| 				return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width)
 | |
| 			end
 | |
| 
 | |
| 			local line = self.lines[y]
 | |
| 			line.dirty = true
 | |
| 			line.text = replace(line.text, x, text, width)
 | |
| 			if fg then
 | |
| 				line.fg = replace(line.fg, x, fg, width)
 | |
| 			end
 | |
| 			if bg then
 | |
| 				line.bg = replace(line.bg, x, bg, width)
 | |
| 			end
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Canvas:writeLine(y, text, fg, bg)
 | |
| 	self.lines[y].dirty = true
 | |
| 	self.lines[y].text = text
 | |
| 	self.lines[y].fg = fg
 | |
| 	self.lines[y].bg = bg
 | |
| end
 | |
| 
 | |
| function Canvas:reset()
 | |
| 	self.regions = nil
 | |
| end
 | |
| 
 | |
| function Canvas:clear(bg, fg)
 | |
| 	local text = _rep(' ', self.width)
 | |
| 	fg = _rep(self.palette[fg or colors.white], self.width)
 | |
| 	bg = _rep(self.palette[bg or colors.black], self.width)
 | |
| 	for i = 1, self.height do
 | |
| 		self:writeLine(i, text, fg, bg)
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Canvas:punch(rect)
 | |
| 	if not self.regions then
 | |
| 		self.regions = Region.new(self.x, self.y, self.ex, self.ey)
 | |
| 	end
 | |
| 	self.regions:subRect(rect.x, rect.y, rect.ex, rect.ey)
 | |
| end
 | |
| 
 | |
| function Canvas:blitClipped(device)
 | |
| 	for _,region in ipairs(self.regions.region) do
 | |
| 		self:blit(device,
 | |
| 			{ x = region[1] - self.x + 1,
 | |
| 				y = region[2] - self.y + 1,
 | |
| 				ex = region[3]- self.x + 1,
 | |
| 				ey = region[4] - self.y + 1 },
 | |
| 			{ x = region[1], y = region[2] })
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Canvas:redraw(device)
 | |
| 	self:reset()
 | |
| 	if #self.layers > 0 then
 | |
| 		for _,layer in pairs(self.layers) do
 | |
| 			self:punch(layer)
 | |
| 		end
 | |
| 		self:blitClipped(device)
 | |
| 	else
 | |
| 		self:blit(device)
 | |
| 	end
 | |
| 	self:clean()
 | |
| end
 | |
| 
 | |
| function Canvas:isDirty()
 | |
| 	for _, line in pairs(self.lines) do
 | |
| 		if line.dirty then
 | |
| 			return true
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Canvas:dirty()
 | |
| 	for _, line in pairs(self.lines) do
 | |
| 		line.dirty = true
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Canvas:clean()
 | |
| 	for _, line in pairs(self.lines) do
 | |
| 		line.dirty = false
 | |
| 	end
 | |
| end
 | |
| 
 | |
| function Canvas:render(device, layers) --- redrawAll ?
 | |
| 	layers = layers or self.layers
 | |
| 	if #layers > 0 then
 | |
| 		self.regions = Region.new(self.x, self.y, self.ex, self.ey)
 | |
| 		local l = Util.shallowCopy(layers)
 | |
| 		for _, canvas in ipairs(layers) do
 | |
| 			table.remove(l, 1)
 | |
| 			if canvas.visible then
 | |
| 				self:punch(canvas)
 | |
| 				canvas:render(device, l)
 | |
| 			end
 | |
| 		end
 | |
| 		self:blitClipped(device)
 | |
| 		self:reset()
 | |
| 	else
 | |
| 		self:blit(device)
 | |
| 	end
 | |
| 	self:clean()
 | |
| end
 | |
| 
 | |
| function Canvas:blit(device, src, tgt)
 | |
| 	src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 }
 | |
| 	tgt = tgt or self
 | |
| 
 | |
| 	for i = 0, src.ey - src.y do
 | |
| 		local line = self.lines[src.y + i]
 | |
| 		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 tgt.y + i > self.ey then -- wrong place to do clipping ??
 | |
| 			--  break
 | |
| 			--end
 | |
| 			device.setCursorPos(tgt.x, tgt.y + i)
 | |
| 			device.blit(t, fg, bg)
 | |
| 		end
 | |
| 	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
 | |
| 
 | |
| function Canvas.convertWindow(win, parent, wx, wy)
 | |
| 	local w, h = win.getSize()
 | |
| 
 | |
| 	win.canvas = Canvas({
 | |
| 		x       = wx,
 | |
| 		y       = wy,
 | |
| 		width   = w,
 | |
| 		height  = h,
 | |
| 		isColor = win.isColor(),
 | |
| 	})
 | |
| 
 | |
| 	function win.clear()
 | |
| 		win.canvas:clear(win.getBackgroundColor(), win.getTextColor())
 | |
| 	end
 | |
| 
 | |
| 	function win.clearLine()
 | |
| 		local _, y = win.getCursorPos()
 | |
| 		win.canvas:write(1,
 | |
| 			y,
 | |
| 			_rep(' ', win.canvas.width),
 | |
| 			win.getBackgroundColor(),
 | |
| 			win.getTextColor())
 | |
| 	end
 | |
| 
 | |
| 	function win.write(str)
 | |
| 		local x, y = win.getCursorPos()
 | |
| 		win.canvas:write(x,
 | |
| 			y,
 | |
| 			str,
 | |
| 			win.getBackgroundColor(),
 | |
| 			win.getTextColor())
 | |
| 		win.setCursorPos(x + #str, y)
 | |
| 	end
 | |
| 
 | |
| 	function win.blit(text, fg, bg)
 | |
| 		local x, y = win.getCursorPos()
 | |
| 		win.canvas:writeBlit(x, y, text, bg, fg)
 | |
| 	end
 | |
| 
 | |
| 	function win.redraw()
 | |
| 		win.canvas:redraw(parent)
 | |
| 	end
 | |
| 
 | |
| 	function win.scroll(n)
 | |
| 		table.insert(win.canvas.lines, table.remove(win.canvas.lines, 1))
 | |
| 		win.canvas.lines[#win.canvas.lines].text = _rep(' ', win.canvas.width)
 | |
| 		win.canvas:dirty()
 | |
| 	end
 | |
| 
 | |
| 	function win.reposition(x, y, width, height)
 | |
| 		win.canvas.x, win.canvas.y = x, y
 | |
| 		win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
 | |
| 	end
 | |
| 
 | |
| 	win.clear()
 | |
| end
 | |
| 
 | |
| function Canvas.scrollingWindow(win, wx, wy)
 | |
| 	local w, h = win.getSize()
 | |
| 	local scrollPos = 0
 | |
| 	local maxScroll = h
 | |
| 
 | |
| 	-- canvas lines are are a sliding window within the local lines table
 | |
| 	local lines = { }
 | |
| 
 | |
| 	local parent
 | |
| 	local canvas = Canvas({
 | |
| 		x       = wx,
 | |
| 		y       = wy,
 | |
| 		width   = w,
 | |
| 		height  = h,
 | |
| 		isColor = win.isColor(),
 | |
| 	})
 | |
| 	win.canvas = canvas
 | |
| 
 | |
| 	local function scrollTo(p, forceRedraw)
 | |
| 		local ms = #lines - canvas.height -- max scroll
 | |
| 		p = math.min(math.max(p, 0), ms)  -- normalize
 | |
| 
 | |
| 		if p ~= scrollPos or forceRedraw then
 | |
| 			scrollPos = p
 | |
| 			for i = 1, canvas.height do
 | |
| 				canvas.lines[i] = lines[i + scrollPos]
 | |
| 			end
 | |
| 			canvas:dirty()
 | |
| 		end
 | |
| 	end
 | |
| 
 | |
| 	function win.blit(text, fg, bg)
 | |
| 		local x, y = win.getCursorPos()
 | |
| 		win.canvas:writeBlit(x, y, text, bg, fg)
 | |
| 		win.redraw()
 | |
| 	end
 | |
| 
 | |
| 	function win.clear()
 | |
| 		lines = { }
 | |
| 		for i = 1, canvas.height do
 | |
| 			lines[i] = canvas.lines[i]
 | |
| 		end
 | |
| 		scrollPos = 0
 | |
| 		canvas:clear(win.getBackgroundColor(), win.getTextColor())
 | |
| 		win.redraw()
 | |
| 	end
 | |
| 
 | |
| 	function win.clearLine()
 | |
| 		local _, y = win.getCursorPos()
 | |
| 
 | |
| 		scrollTo(#lines - canvas.height)
 | |
| 		win.canvas:write(1,
 | |
| 			y,
 | |
| 			_rep(' ', win.canvas.width),
 | |
| 			win.getBackgroundColor(),
 | |
| 			win.getTextColor())
 | |
| 		win.redraw()
 | |
| 	end
 | |
| 
 | |
| 	function win.redraw()
 | |
| 		if parent and canvas.visible then
 | |
| 			local x, y = win.getCursorPos()
 | |
| 			for i = 1, canvas.height do
 | |
| 				local line = canvas.lines[i]
 | |
| 				if line and line.dirty then
 | |
| 					parent.setCursorPos(canvas.x, canvas.y + i - 1)
 | |
| 					parent.blit(line.text, line.fg, line.bg)
 | |
| 					line.dirty = false
 | |
| 				end
 | |
| 			end
 | |
| 			win.setCursorPos(x, y)
 | |
| 		end
 | |
| 	end
 | |
| 
 | |
| 	-- doesn't support negative scrolling...
 | |
| 	function win.scroll(n)
 | |
| 		for _ = 1, n do
 | |
| 			lines[#lines + 1] = {
 | |
| 				text = _rep(' ', canvas.width),
 | |
| 				fg = _rep(canvas.palette[win.getTextColor()], canvas.width),
 | |
| 				bg = _rep(canvas.palette[win.getBackgroundColor()], canvas.width),
 | |
| 			}
 | |
| 		end
 | |
| 
 | |
| 		while #lines > maxScroll do
 | |
| 			table.remove(lines, 1)
 | |
| 		end
 | |
| 
 | |
| 		scrollTo(maxScroll, true)
 | |
| 		win.redraw()
 | |
| 	end
 | |
| 
 | |
| 	function win.scrollDown()
 | |
| 		scrollTo(scrollPos + 1)
 | |
| 		win.redraw()
 | |
| 	end
 | |
| 
 | |
| 	function win.scrollUp()
 | |
| 		scrollTo(scrollPos - 1)
 | |
| 		win.redraw()
 | |
| 	end
 | |
| 
 | |
| 	function win.setMaxScroll(ms)
 | |
| 		maxScroll = ms
 | |
| 	end
 | |
| 
 | |
| 	function win.setParent(p)
 | |
| 		parent = p
 | |
| 	end
 | |
| 
 | |
| 	function win.write(str)
 | |
| 		str = tostring(str) or ''
 | |
| 
 | |
| 		local x, y = win.getCursorPos()
 | |
| 		scrollTo(#lines - canvas.height)
 | |
| 		win.blit(str,
 | |
| 			_rep(canvas.palette[win.getTextColor()], #str),
 | |
| 			_rep(canvas.palette[win.getBackgroundColor()], #str))
 | |
| 		win.setCursorPos(x + #str, y)
 | |
| 	end
 | |
| 
 | |
| 	function win.reposition(x, y, width, height)
 | |
| 		win.canvas.x, win.canvas.y = x, y
 | |
| 		win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
 | |
| 	end
 | |
| 
 | |
| 	win.clear()
 | |
| end
 | |
| return Canvas
 | 
