Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- the table that contains all of the running windows
- local windows = {}
- -- this is triggered by pressing left alt and allows you to drag windows without triggering them
- -- also to open their right-click menu (if any)
- local altmode = false
- -- the message that briefly appears at the top of the screen if there's a problem
- local warningMsg = ''
- -- just a nice boolean where i can stop the program safely at any point
- local running = true
- -- localizations, to keep everything nice and neat
- local init, update, draw, exit
- local newImage, newWindow, newImageWindow, newTitlebar
- local bgMenu, imageMenu, colorMenu
- -- convenience for drawing rectangles
- 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
- -- convenience for checking if x and y is in the area at position
- -- ax, ay with width aw and height ah
- 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
- -- just combines tables, e.g. combine({1,2}, {3,4}) --> {1,2,3,4}
- local function combine(t1, t2)
- for i=1, #t2 do
- table.insert(t1, t2)
- end
- return t1
- end
- -- convenience for making a menu option
- 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
- -- convenience for making a context menu
- -- which are menus that have options one atop another
- 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
- -- convenience for a menu separator
- local function separator()
- return menuOption('---', 1,1, function() end)
- end
- -- to get user input in an attractive matter.
- local function prompt(text, placeholder)
- local t = term
- draw() -- i use this sometimes to "clear" the screen
- 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
- -- for opening menus at position x, y with a table of menu options
- -- also passes optional arguments to menu functions
- -- logic for this one is really weird lol
- local function openMenu(x, y, menu, ...)
- y = y + 1 -- i dunno
- --[[
- 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
- -- also limit to screen if needed
- 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
- --i've stored it in a function so i can call it whenever i need to
- 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
- -- convenience for adding new windows
- local function addWindow(window)
- table.insert(windows, 1, window)
- end
- -- convenience for making a new warning
- local function warning(message)
- warningMsg = message
- end
- -- the titlebar class
- -- basically, i wanted to keep all of the window positioning stuff in the titlebar
- -- so i can know when the title of the window was clicked more efficiently, and keep it from
- -- all of what's actually in the window
- function newTitleBar(obj)
- -- using functional OO because it's neater and fits the rest of the program
- obj = obj or {}
- local titlebar = {
- x = 3;
- y = 2;
- width = 12;
- text = 'image';
- }
- -- convenience for returning the dimensions of the rectangle, for use with checkArea()
- -- also allows adding dimensions, so I can check specific areas
- 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
- -- THIS IS WHERE THE REAL FUN BEGINS
- 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;
- }
- -- pass on some of the object properties to the titlebar
- 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
- -- same as titlebar.rect
- 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
- -- using a setname function to set the name of the window
- -- and the title text at the same time
- function window:setName(name)
- self.name = name
- self.title.text = name
- end
- -- again, managing both the window and the title for safety
- 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
- -- convenience maximizing
- -- stores previous size and position in nsize and npos
- 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
- -- convenience restoring
- 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
- -- all windows must have an onClick event
- -- called when the user clicks anywhere
- function window:onClick(button, x, y, index)
- -- remove dragging and resizing properties on click
- -- if we want to drag or resize again, they will be reenabled by the end of this
- self.drag = nil
- self.resize = false
- -- first, we check if the mouse is inside the entirety of the window.
- if checkArea(x, y, self:rect()) then
- -- if we're in the window, awesome. are we in the titlebar?
- -- or is altmode on? cool
- if checkArea(x, y, self.title:rect())
- or altmode then
- -- do this if we used the left mouse button
- if button == 1 then
- -- be able to access the "background" menu when maximized
- -- via the top-left corner of the window
- if self.maximized and x == self.title.x and y == self.title.y then
- openMenu(x, y, bgMenu(), x, y)
- end
- -- turn on dragging (only if we aren't maximized)
- -- record the dragging offset
- if not self.maximized then
- self.drag = {
- x = self.title.x - x;
- y = self.title.y - y;
- }
- end
- -- check double clicks using a local lastClick variable
- 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
- -- returning "focusme" brings the window to the front of the windows table
- -- the window at the front has focus and is drawn last
- return 'focusme'
- -- this is for checking if the mouse is at the outside bottom-right corner of the window.
- elseif x == self.title.x + self.width and y == self.title.y + self.height + 1 then
- -- turn on resizing, and set a timer to hide the resize "handle"
- self.resize = true
- self.resizeTimer = os.startTimer(1)
- return 'focusme'
- end
- end
- -- called on a mouse drag
- function window:onDrag(button, x, y)
- -- make sure we have focus
- if windows[1] == self then
- -- drag actions should only happen if we're maximized
- if not self.maximized then
- -- if we're in dragging mode, go to the dragging posision
- if self.drag then
- self.title.x = x + self.drag.x
- self.title.y = y + self.drag.y
- -- if we're in resize mode, resize to wherever the mouse is
- -- WITH MATHS!!!!
- elseif self.resize then
- local width, height = x - self.title.x, y - self.title.y - 1 -- -1 to account for the titlebar
- self:setSize(width, height)
- -- self.resizeTimer = os.startTimer(1)
- end
- end
- end
- end
- -- called on a key event - not used by default
- function window:onKey(key)
- end
- -- called on a char event - again, not used
- function window:onChar(char)
- end
- -- called on a timer event
- function window:onTimer(t)
- -- only to hide the resize handle
- if t == self.resizeTimer then
- self.resizeTimer = nil
- end
- end
- -- WELL WE HAVE TO ACTUALLY FRIGGEN DRAW THE THING
- 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 window backing
- -- i might make a property for this so i can have transparent windows if i want
- 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
- -- convenience for making a window to be removed from the windows table
- function window:close()
- self.remove = true
- end
- return setmetatable(obj, {__index = window})
- end
- -- a nice example of how to make your own program/function
- -- very simple, just has a little pixel moving around the window
- -- with the arrow keys
- function newTestProgram(obj)
- -- make a local clone of the window so we can call its functions later if needed
- local super = newWindow()
- local window = setmetatable({}, {__index = super})
- -- make a local player variable to keep the "player" position
- local player = {x = 3, y = 2}
- -- convenience
- local function move(x, y)
- player.x = player.x + x
- player.y = player.y + y
- end
- -- overwrite the key event to include our moving functions
- function window:onKey(k)
- if k == keys.right then
- move(1, 0)
- elseif k == keys.left then
- move(-1, 0)
- elseif k == keys.down then
- move(0, 1)
- elseif k == keys.up then
- move(0, -1)
- end
- end
- -- to draw our character
- function window:draw()
- -- we need to include the super:draw() with self
- -- otherwise no titlebar/window backing
- super.draw(self)
- -- limit the character to the window width and height
- -- width and height is already defined in the default window, so no need
- -- to define it ourselves
- player.x =
- player.x < 1 and 1 or
- player.x > self.width and self.width or
- player.x
- player.y =
- player.y < 1 and 1 or
- player.y > self.height and self.height or
- player.y
- local t = term
- t.setBackgroundColor(colors.gray)
- t.setTextColor(colors.white)
- t.setCursorPos(self.title.x + player.x - 1, self.title.y + player.y)
- t.write '@'
- end
- window:setName('My Wonderful Game Thing')
- return setmetatable(obj or {}, {__index = window})
- end
- -- various context menus
- 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
- -- here we just make a new window on startup
- -- the commented out code makes a nice little display for the canvas program
- 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(newWindow{name='woop'})
- addWindow(newTestProgram())
- end
- function update(ev, p1, p2, p3)
- -- set the warning message to nothing on click or key
- if ev == 'key' or ev == 'mouse_click' then
- warningMsg = ''
- end
- if ev == 'mouse_click' then
- -- i'm using this function wrapper because of *
- (function()
- local button, x, y = p1, p2, p3
- -- go through the windows
- for i=1, #windows do
- local window = windows[i]
- -- check if the window wants focus
- if window:onClick(button, x, y, i) == 'focusme' then
- table.insert(windows, 1, table.remove(windows, i))
- return -- * this
- -- i don't want to use a stupid boolean
- -- to check if i've found a window to focus on.
- end
- end
- if button == 2 then
- openMenu(x, y, bgMenu(), x, y)
- end
- end)()
- elseif ev == 'mouse_drag' then
- -- trigger drag events
- for i=1, #windows do
- windows[i]:onDrag(p1, p2, p3)
- end
- elseif ev == 'key' then
- -- trigger key event on the focused window
- windows[1]:onKey(p1)
- -- also cycle windows when i press the tab key
- if p1 == keys.tab then
- table.insert(windows, table.remove(windows, 1))
- end
- elseif ev == 'char' then
- -- trigger char event on the focused window
- windows[1]:onChar(p1)
- elseif ev == 'timer' then
- -- trigger timer events
- for i=1, #windows do
- windows[i]:onTimer(p1)
- end
- end
- -- trigger alt mode
- -- it's not really a toggle, more of an activation on this current event
- if ev == 'key' and (p1 == keys.leftAlt or p1 == keys.rightAlt) then
- altmode = true
- else
- altmode = false
- end
- -- remove any windows that are marked
- -- go backwards to prevent problems
- for i=#windows, 1, -1 do
- if windows[i].remove == true then
- table.remove(windows, i)
- end
- end
- -- make the user able to terminate the program
- -- for whatever reason
- if ev == 'terminate' then
- running = false
- end
- -- was for testing
- --[[
- if ev == 'key' and p1 == keys.backspace then
- running = false
- end
- --]]
- end
- function draw()
- local t = term
- local w,h = term.getSize()
- -- draw the blue background
- t.setBackgroundColor(colors.blue)
- t.clear()
- -- only draw the warning message if it ain't empty
- if warningMsg ~= '' then
- t.setCursorPos(1,1)
- t.setBackgroundColor(colors.gray)
- t.setTextColor(colors.white)
- t.clearLine()
- t.write(warningMsg)
- end
- -- draw 'dem windows yo
- for i=#windows, 1, -1 do
- windows[i]:draw()
- end
- end
- function exit()
- -- give the user a nice thank you message. :)
- 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()
- -- main event loop
- -- it's very nice
- init()
- while running do
- draw()
- update(os.pullEventRaw())
- end
- exit()
- end
- -- PROGRAM EXECUTION!
- main()
Advertisement
Add Comment
Please, Sign In to add comment