Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local windows = {}
- local running = true
- local altmode = false
- local warningMsg = ''
- --local warningTime = 0
- local init, update, draw, exit
- local newImage, newWindow, newImageWindow, newTitlebar
- local bgMenu, imageMenu, colorMenu
- local function rect(x, y, width, height, color, noshadow)
- local spaces = string.rep(' ', width)
- local t = term
- local r = function(x, y, color)
- t.setBackgroundColor(color)
- for i=y, y + height - 1 do
- t.setCursorPos(x, i)
- t.write(spaces)
- end
- end
- if not noshadow then r(x + 1, y + 1, colors.black) end
- r(x, y, color)
- end
- local function checkArea(x, y, ax, ay, aw, ah)
- return
- x >= ax and
- x < ax + aw and
- y >= ay and
- y < ay + ah
- end
- -- i thought i would need this but i guess not
- local function combine(t1, t2)
- for i=1, #t2 do
- table.insert(t1, t2)
- end
- return t1
- end
- local function tohex(color)
- local n = math.log(color) / math.log(2) + 1
- return string.sub('0123456789abcdef', n, n)
- end
- local function tocolor(hex)
- local n = tonumber(hex, 16)
- return n and 2 ^ n or 0
- end
- local function menuOption(text, x, y, action, color, bg)
- return {
- text = text;
- x = x;
- y = y;
- action = action;
- color = color or colors.lightGray;
- bg = bg or colors.gray;
- }
- end
- local function contextMenu(options)
- for i=1, #options do
- local opt = options[i]
- opt.x = 1
- opt.y = i
- opt.text = ' '..opt.text..' '
- end
- return options
- end
- local function separator()
- return menuOption('---', 1,1, function() end)
- end
- local function prompt(text, placeholder)
- local t = term
- draw()
- rect(1, 1, t.getSize(), 1, colors.gray, true)
- t.setCursorPos(2,1)
- t.setTextColor(colors.white)
- t.write(text)
- if placeholder then
- for i=1, #placeholder do
- os.queueEvent('char', placeholder:sub(i,i))
- end
- end
- return read()
- end
- local function openMenu(x, y, menu, ...)
- y = y + 1
- --[[
- menu table format
- {
- text = 'OptionText';
- x = 1;
- y = 1;
- color = someTextColor;
- bg = someBackgroundColor;
- action = function()
- doStuff()
- if i decide to change the menu then
- return a new menu
- end
- end;
- }
- ]]
- local ox, oy
- local width = 1
- local height = 1
- local t = term
- local w,h = term.getSize()
- local function offsetOptions()
- -- find the longest menu option and base the width off of that
- for i=1, #menu do
- local opt = menu[i]
- local cur = opt.x + #opt.text - 1
- if cur > width then
- width = cur
- end
- end
- -- same for height
- for i=1, #menu do
- local opt = menu[i]
- if opt.y > height then
- height = opt.y
- end
- end
- -- add the x and y offset to the options
- ox = x + width - 1 > w and w - width + 1 or x
- oy = y + height - 1 > h and h - height + 1 or y
- for i=1, #menu do
- local opt = menu[i]
- opt.x = opt.x + ox - 1
- opt.y = opt.y + oy - 1
- end
- end
- offsetOptions()
- draw()
- t.setCursorBlink(false)
- rect(ox, oy - 1, width, height + 1, colors.gray)
- for i=1, #menu do
- local opt = menu[i]
- rect(opt.x, opt.y, #opt.text, 1, opt.bg, true)
- t.setCursorPos(opt.x, opt.y)
- t.setTextColor(opt.color)
- t.write(opt.text)
- end
- local _, button, mx, my = os.pullEvent('mouse_click')
- for i=1, #menu do
- local opt = menu[i]
- if checkArea(mx, my, opt.x, opt.y, #opt.text, 1) then
- local res = opt.action(...)
- if type(res) == 'table' then
- draw()
- openMenu(x, y, res, ...)
- end
- break
- end
- end
- end
- local function addWindow(window)
- table.insert(windows, 1, window)
- end
- local function warning(message, time)
- warningMsg = message
- warningTime = os.clock()
- end
- function newTitleBar(obj)
- obj = obj or {}
- local titlebar = {
- x = 3;
- y = 2;
- width = 12;
- text = 'image';
- }
- function titlebar:rect(dx, dy, dw, dh)
- return
- self.x + (dx or 0),
- self.y + (dy or 0),
- self.width + (dw or 0),
- 1 + (dh or 0)
- end
- function titlebar:draw()
- local t = term
- t.setTextColor(windows[1].title == self and colors.white or colors.lightGray)
- -- limit the width if needed
- self.width = self.width > 5 and self.width or 5
- -- limit the text if needed
- local text =
- #self.text > self.width - 2 and
- self.text:sub(1, self.width - 4) .. '..' or
- self.text
- -- draw the title background
- rect(self.x, self.y, self.width, 1, colors.gray)
- -- draw the title text
- t.setCursorPos(self.x + self.width/2 - #text/2, self.y)
- -- t.setCursorPos(self.x + 1, self.y)
- t.write(text)
- end
- return setmetatable(obj, {__index = titlebar})
- end
- function newImage(obj)
- obj = obj or {}
- function obj:__call(x, y, value)
- if not self[y] then
- self[y] = {}
- end
- self[y][x] = value or self[y][x]
- return self[y][x]
- end
- return setmetatable(obj, obj)
- end
- function newWindow(obj)
- obj = obj or {}
- local window = {
- name = 'window';
- width = 15;
- height = 8;
- drag = nil;
- resize = false;
- resizeTimer = nil;
- lastClick = nil;
- maximized = false;
- remove = false;
- }
- window.title = newTitleBar{
- x = obj.x or 2;
- y = obj.y or 3;
- text = obj.name or window.name;
- width = obj.width or window.width;
- }
- local lastClick
- function window:rect(dx, dy, dw, dh)
- return
- self.title.x + (dx or 0),
- self.title.y + (dy or 0),
- self.width + (dw or 0),
- self.height + (dh or 0)
- end
- function window:setName(name)
- self.name = name
- self.title.text = name
- end
- function window:setSize(width, height)
- self.width = width > 1 and width or 1
- self.height = height > 1 and height or 1
- self.title.width = self.width
- end
- function window:maximize()
- if not self.maximized then
- self.maximized = true
- local w,h = term.getSize()
- -- "n" in this case stands for "normal"
- self.nsize = {self.width, self.height}
- self.npos = {self.title.x, self.title.y}
- self.title.x, self.title.y = 1, 1
- self:setSize(w, h - 1)
- end
- end
- function window:restore()
- if self.maximized then
- self.maximized = false
- self:setSize(unpack(self.nsize))
- self.title.x, self.title.y = unpack(self.npos)
- end
- end
- function window:onClick(button, x, y, index)
- self.drag = nil
- self.resize = false
- if checkArea(x, y, self:rect()) then
- if checkArea(x, y, self.title:rect(0, 0, -1, 0))
- or altmode then
- if button == 1 then
- if self.maximized and x == self.title.x and y == self.title.y then
- openMenu(x, y, bgMenu(), x, y)
- end
- if not self.maximized then
- self.drag = {
- x = self.title.x - x;
- y = self.title.y - y;
- }
- end
- if lastClick then
- if os.clock() - lastClick <= 0.3 then
- if self.maximized then
- self:restore()
- else
- self:maximize()
- end
- end
- end
- lastClick = os.clock()
- end
- end
- return 'focusme'
- elseif x == self.title.x + self.width and y == self.title.y + self.height + 1 then
- self.resize = true
- self.resizeTimer = os.startTimer(1)
- return 'focusme'
- end
- end
- function window:onDrag(button, x, y)
- if windows[1] == self then
- if not self.maximized then
- if self.drag then
- self.title.x = x + self.drag.x
- self.title.y = y + self.drag.y
- elseif self.resize then
- local width, height = x - self.title.x, y - self.title.y - 1
- self:setSize(width, height)
- -- self.resizeTimer = os.startTimer(1)
- end
- elseif self.drag then
- if self.drag.y > 0 then
- self:restore()
- self.title.x = x + self.drag.x
- self.title.y = y + self.drag.y
- end
- end
- end
- end
- function window:onKey(key)
- end
- function window:onChar(char)
- end
- function window:onTimer(t)
- if t == self.resizeTimer then
- self.resizeTimer = nil
- end
- end
- function window:draw()
- local t = term
- local w,h = t.getSize()
- local ox, oy = self.title.x, self.title.y + 1
- -- draw the titlebar
- self.title:draw()
- -- draw the backing
- rect(self.title.x, self.title.y + 1, self.width, self.height, colors.gray)
- -- also draw a little dot to show we can access the background menu when maximized
- if self.maximized then
- t.setCursorPos(self.title.x, self.title.y)
- t.setBackgroundColor(colors.gray)
- t.setTextColor(colors.white)
- t.write '.'
- end
- -- draw the resize handle (if we're timed)
- if self.resizeTimer then
- t.setCursorPos(self.title.x + self.width, self.title.y + self.height + 1)
- t.setBackgroundColor(colors.black)
- t.setTextColor(colors.gray)
- t.write '%'
- end
- end
- function window:close()
- self.remove = true
- end
- if obj then
- for i,v in pairs(obj) do
- window[i] = v
- end
- end
- return window
- end
- function newImageWindow(obj)
- obj = obj or {}
- local super = newWindow()
- local window = {
- mode = 'paint';
- modes = {'paint','char'};
- -- modes = {'paint','char','select'};
- color = colors.lightGray;
- textColor = colors.gray;
- pos = {x=1, y=1};
- savePath = nil;
- image = newImage();
- }
- setmetatable(window, {__index = super})
- function window:paint(mode, x, y)
- -- lucky for me, modes correspond nicely with button numbering
- local ix, iy = x - self.title.x + 1, y - self.title.y
- if checkArea(ix, iy, 1, 1, self.width, self.height) then
- local pixel = self.image(ix, iy)
- if mode == 1 then -- color
- pixel.color
- = self.color
- elseif mode == 2 then -- erase
- pixel.color = 0
- pixel.char = ' '
- elseif mode == 3 then -- pick
- self.color = pixel.color > 0 and pixel.color or self.color
- end
- end
- end
- function window:onClick(button, x, y)
- local m = self.maximized
- super.onClick(self, button, x, y)
- if checkArea(x, y, self:rect(0, 0, 0, 1)) then
- if checkArea(x, y, self.title:rect(0, 0, -1, 0))
- or altmode then
- if button == 2 then
- openMenu(x, y, imageMenu(), self)
- end
- elseif x == self.title.x + self.title.width - 1 and y == self.title.y then
- openMenu(x + 1, y, colorMenu(), self)
- else
- if self.mode == 'paint' then
- if self.maximized == m then
- self:paint(button, x, y)
- end
- elseif self.mode == 'char' then
- self.pos.x = x - self.title.x + 1
- self.pos.y = y - self.title.y
- end
- end
- return 'focusme'
- end
- end
- function window:onDrag(button, x, y)
- super.onDrag(self, button, x, y)
- if windows[1] == self and self.mode == 'paint' then
- self:paint(button, x, y)
- end
- end
- function window:onKey(key)
- if key == keys.leftCtrl then
- table.insert(self.modes, table.remove(self.modes, 1))
- self.mode = self.modes[1]
- return
- end
- if self.mode == 'char' then
- local x, y = self.pos.x, self.pos.y
- local function forward()
- self.pos.y = (x == self.width and y < self.height) and y + 1 or y
- self.pos.x = x < self.width and x + 1 or
- y < self.height and 1 or x
- end
- local function back()
- self.pos.y = (x == 1 and y > 1) and y - 1 or y
- self.pos.x = x > 1 and x - 1 or
- y > 1 and self.width or x
- end
- if key == keys.right then
- forward()
- elseif key == keys.left then
- back()
- elseif key == keys.down then
- self.pos.y = y < self.height and y + 1 or y
- elseif key == keys.up then
- self.pos.y = y > 1 and y - 1 or y
- elseif key == keys.backspace then
- back()
- self.image(self.pos.x, self.pos.y).char = ' '
- elseif key == keys.enter then
- if self.pos.y < self.height then
- self.pos.x = 1
- self.pos.y = y + 1
- end
- elseif key == keys.home then
- self.pos.x = 1
- elseif key == keys['end'] then
- self.pos.x = self.width
- end
- end
- end
- function window:onChar(char)
- if self.mode == 'char' then
- local x, y = self.pos.x, self.pos.y
- local pixel = self.image(x, y)
- if pixel.color == 0 then
- pixel.color = self.color
- end
- pixel.char = char
- pixel.textColor = self.textColor
- if x < self.width then
- self.pos.x = x + 1
- elseif y < self.height then
- self.pos.x = 1
- self.pos.y = y + 1
- end
- end
- end
- function window:draw()
- super.draw(self)
- local t = term
- local w,h = t.getSize()
- local ox, oy = self.title.x, self.title.y + 1
- -- draw the image
- for x=1, self.width do
- for y=1, self.height do
- t.setCursorPos(x + ox - 1, y + oy - 1)
- local pixel = self.image(x, y)
- if not pixel then
- pixel = self.image(x, y, {
- color = 0;
- textColor = 0;
- char = ' ';
- })
- end
- if pixel.color > 0 then
- t.setBackgroundColor(pixel.color)
- if pixel.textColor > 0 then t.setTextColor(pixel.textColor) end
- t.write(pixel.char)
- else
- t.setBackgroundColor(colors.gray)
- t.setTextColor(colors.lightGray)
- t.write '.'
- end
- end
- end
- -- draw our color on the titlebar
- t.setCursorPos(self.title.x + self.title.width - 1, self.title.y)
- if self.mode == 'paint' then
- t.setBackgroundColor(self.color)
- t.write ' '
- elseif self.mode == 'char' then
- t.setBackgroundColor(self.textColor == colors.gray and colors.lightGray or colors.gray)
- t.setTextColor(self.textColor)
- t.write 'A'
- end
- -- limit the cursor position if we're on char mode
- if mode == 'char' then
- local x, y = self.pos.x, self.pos.y
- self.pos.x =
- x > self.width and self.width or
- x < 1 and 1 or x
- self.pos.y =
- y > self.height and self.height or
- y < 1 and 1 or y
- end
- -- reposition our cursor
- -- also choose a cursor color that doesn't blend with the background
- local pixel = self.image(self.pos.x, self.pos.y)
- t.setTextColor(
- (self.textColor == pixel.color or self.textColor == colors.gray and pixel.color == 0) and
- (self.textColor == colors.white and colors.black or colors.white) or
- self.textColor
- )
- t.setCursorPos(self.pos.x + self.title.x - 1, self.pos.y + self.title.y)
- t.setCursorBlink(self.mode == 'char')
- end
- local function imageData()
- local self = window
- local data = ''
- for y=1, self.height do
- for x=1, self.width do
- local pixel = self.image(x, y)
- local color = pixel.color > 0 and tohex(pixel.color) or ' '
- local textColor = pixel.textColor > 0 and tohex(pixel.textColor) or ' '
- data = data .. color..textColor..pixel.char
- end
- data = data..'\n'
- end
- return data
- end
- local function save(data)
- local self = window
- local path = self.savePath
- if not path then
- path = prompt('Save as: ', window.name)
- end
- local file = fs.open(path, 'w')
- if file then
- self.savePath = path
- self:setName(path)
- local folder = path:match('(.*/).+')
- if folder and not fs.exists(folder) then
- fs.makeDir(folder)
- end
- file.write(data)
- file.close()
- return true
- else
- return false, 'File is read only'
- end
- end
- function window:saveImage()
- return save('@canvas\n'..imageData())
- end
- function window:saveImageObject()
- local script = [[
- --@canvasobj
- local data = %s
- local image = {}
- local mt = {}
- local function tocolor(hex)
- local n = tonumber(hex, 16)
- return n and 2 ^ n or 0
- end
- local y = 1
- for line in data:gmatch('[^\n]+') do
- local x = 1
- image[y] = {}
- for color, textColor, char in line:gmatch('(.)(.)(.)') do
- color = tocolor(color)
- textColor = tocolor(textColor)
- image[y][x] = {
- color = color;
- textColor = textColor;
- char = char;
- }
- x = x + 1
- end
- y = y + 1
- end
- function image:draw(ox, oy)
- local t = term
- for y=1, #self do
- for x=1, #self[y] do
- local pixel = self[y][x]
- t.setCursorPos(x + ox - 1, y + oy - 1)
- if pixel.color then
- t.setBackgroundColor(pixel.color)
- end
- if pixel.textColor then
- t.setTextColor(pixel.textColor)
- end
- t.write(pixel.char)
- end
- end
- end
- return image]]
- local content = ''
- -- remove that extra whitespace
- for line in script:gmatch('[^\n]+') do
- content = content .. line:gsub('^\t\t\t','') .. '\n'
- end
- content = content:format('[['..imageData()..']]')
- return save(content)
- end
- function window:loadImage(path)
- local file = fs.open(path, 'r')
- if file then
- self.image = newImage()
- local line = file.readLine()
- if line:match('@canvasobj') then -- is canvas object
- local image = loadstring(file.readAll())()
- for y=1, #image do
- for x=1, #image[y] do
- local pixel = image[y][x]
- self.image(x, y, {
- color = pixel.color;
- textColor = pixel.textColor;
- char = pixel.char;
- })
- end
- end
- elseif line:match('@canvas') then -- is raw canvas format image
- local y = 1
- for line in file.readLine do
- local x = 1
- for color, textColor, char in line:gmatch('(.)(.)(.)') do
- color = color ~= ' ' and tocolor(color) or 0
- textColor = textColor ~= ' ' and tocolor(textColor) or 0
- self.image(x, y, {
- color = color;
- textColor = textColor;
- char = char;
- })
- x = x + 1
- end
- y = y + 1
- end
- else -- is vanilla/native image(?)
- local y = 1
- for line in file.readLine do
- local x = 1
- for color in line:gmatch('.') do
- self.image(x, y, {
- color = tocolor(color) or 0;
- textColor = colors.white;
- char = ' ';
- })
- x = x + 1
- end
- y = y + 1
- end
- end
- self.height = #self.image
- self.width = #self.image[1]
- self.title.width = self.width
- self:setName(path)
- return true, file.close()
- else
- return false, 'File does not exist'
- end
- end
- for x=1, window.width do
- for y=1, window.height do
- window.image(x, y, {
- color = 0;
- textColor = 0;
- char = ' ';
- })
- end
- end
- window:setName(obj.name or '')
- window:setSize(obj.width or 15, obj.height or 8)
- window.title.x = obj.x or 4
- window.title.y = obj.y or 3
- if obj then
- for i,v in pairs(obj) do
- window[i] = v
- end
- end
- return window
- end
- function newFileWindow(obj)
- obj = obj or {}
- -- stuff
- end
- function bgMenu()
- return contextMenu{
- menuOption('New..', 1, 1, function(mx, my)
- local name = prompt('Image Name: ')
- if #name > 0 then
- local window = newImageWindow{
- name = shell.resolve(name);
- x = mx - 6;
- y = my;
- }
- addWindow(window)
- else
- -- send a warning
- end
- end,
- colors.lime);
- menuOption('Open..', 1, 2, function(window)
- local path = prompt('Image Path: ')
- if #path > 0 then
- local window = newImageWindow()
- local ok, err = window:loadImage(path)
- if not ok then
- warning(err)
- else
- addWindow(window)
- end
- end
- end,
- colors.orange);
- separator();
- menuOption('Exit', 1, 3, function()
- running = false
- end,
- colors.red);
- }
- end
- function imageMenu()
- return contextMenu{
- menuOption('Load..', 1,1, function(window)
- local path = prompt('Image Path: ')
- if #path > 0 then
- local ok, err = window:loadImage(path)
- if not ok then
- warning(err)
- end
- else
- -- send a warning
- end
- end,
- colors.magenta);
- menuOption('Save', 1,1, function(window)
- local ok, err = window:saveImage()
- if not ok then
- warning(err)
- end
- end,
- colors.lime);
- menuOption('Save as Script Object', 1,1, function(window)
- local ok, err = window:saveImageObject()
- if not ok then
- warning(err)
- end
- end,
- colors.orange);
- menuOption('Rename', 1,1, function(window)
- local name = prompt('New Name: ', window.name)
- window:setName(name)
- end,
- colors.orange);
- separator();
- menuOption('Close', 1,1, function(window)
- window:close()
- end,
- colors.red);
- }
- end
- function colorMenu()
- local grid = {
- ---[[
- {'red', 'lime', 'blue', 'white'},
- {'orange', 'lightBlue', 'cyan', 'lightGray'},
- {'yellow', 'purple', 'green', 'gray'},
- {'pink', 'magenta', 'brown', 'black'}
- --]]
- }
- local options = {}
- for y=1, #grid do
- for x=1, #grid[y] do
- local color = colors[grid[y][x]]
- table.insert(options, menuOption(' ', x, y, function(window)
- if window.mode == 'paint' then
- window.color = color
- elseif window.mode == 'char' then
- window.textColor = color
- end
- end,
- colors.white, color))
- end
- end
- return options
- end
- function init()
- --[[
- local function new(n)
- local image = newImageWindow{
- x = #windows*-2 + 11;
- y = #windows*2 + 2;
- name = n;
- }
- end
- local words = {'hello', 'and', 'welcome', 'to', 'canvas'}
- for i=5, 1, -1 do
- new(words[i])
- end
- --]]
- addWindow(newImageWindow{name='untitled'})
- end
- function update(ev, p1, p2, p3)
- warningMsg = ''
- if ev == 'mouse_click' then
- (function()
- local button, x, y = p1, p2, p3
- for i=1, #windows do
- local window = windows[i]
- if window:onClick(button, x, y, i)== 'focusme' then
- table.insert(windows, 1, table.remove(windows, i))
- return
- end
- end
- if button == 2 then
- openMenu(x, y, bgMenu(), x, y)
- end
- end)()
- elseif ev == 'mouse_drag' then
- for i=1, #windows do
- windows[i]:onDrag(p1, p2, p3)
- end
- elseif ev == 'key' then
- windows[1]:onKey(p1)
- if p1 == keys.tab then
- table.insert(windows, table.remove(windows, 1))
- end
- elseif ev == 'char' then
- windows[1]:onChar(p1)
- elseif ev == 'timer' then
- for i=1, #windows do
- windows[i]:onTimer(p1)
- end
- if p1 == warningTimer then
- warningMsg = ''
- end
- end
- if ev == 'key' and p1 == keys.leftAlt or p1 == keys.rightAlt then
- altmode = true
- else
- altmode = false
- end
- for i=#windows, 1, -1 do
- if windows[i].remove == true then
- table.remove(windows, i)
- end
- end
- --[[
- if ev == 'key' and p1 == keys.backspace then
- running = false
- end
- if ev == 'terminate' then
- running = false
- end
- --]]
- end
- function draw()
- local t = term
- local w,h = term.getSize()
- t.setBackgroundColor(colors.blue)
- t.clear()
- if warningMsg ~= '' then
- t.setCursorPos(1,1)
- t.setBackgroundColor(colors.gray)
- t.setTextColor(colors.white)
- t.clearLine()
- t.write(warningMsg)
- end
- for i=#windows, 1, -1 do
- windows[i]:draw()
- end
- end
- function exit()
- local t = term
- t.setCursorPos(1,1)
- t.setBackgroundColor(colors.black)
- t.setTextColor(colors.white)
- t.clear()
- print 'Thanks for using canvas! -Kingdaro'
- end
- local function main()
- init()
- while running do
- draw()
- update(os.pullEventRaw())
- end
- exit()
- end
- main()
Add Comment
Please, Sign In to add comment