mirror of
				https://github.com/kepler155c/opus
				synced 2025-10-30 23:23:03 +00:00 
			
		
		
		
	Ui enhancements 2.0 (#31)
* canvas overhaul * minor tweaks * list mode for overview * bugfixes + tweaks for editor 2.0 * minor tweaks * more editor work * refactor + new transitions * use layout() where appropriate and cleanup * mouse triple click + textEntry scroll ind * cleanup * cleanup + theme editor * color rework + cleanup * changes for deprecated ui methods * can now use named colors
This commit is contained in:
		| @@ -1,3 +1,5 @@ | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local Array = { } | ||||
|  | ||||
| function Array.filter(it, f) | ||||
| @@ -14,9 +16,11 @@ function Array.removeByValue(t, e) | ||||
| 	for k,v in pairs(t) do | ||||
| 		if v == e then | ||||
| 			table.remove(t, k) | ||||
| 			break | ||||
| 			return e | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| Array.find = Util.find | ||||
|  | ||||
| return Array | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local fs    = _G.fs | ||||
| local shell = _ENV.shell | ||||
|  | ||||
| local Config = { } | ||||
|  | ||||
| @@ -25,23 +24,6 @@ function Config.load(fname, data) | ||||
| 	return data | ||||
| end | ||||
|  | ||||
| function Config.loadWithCheck(fname, data) | ||||
| 	local filename = 'usr/config/' .. fname | ||||
|  | ||||
| 	if not fs.exists(filename) then | ||||
| 		Config.load(fname, data) | ||||
| 		print() | ||||
| 		print('The configuration file has been created.') | ||||
| 		print('The file name is: ' .. filename) | ||||
| 		print() | ||||
| 		_G.printError('Press enter to configure') | ||||
| 		_G.read() | ||||
| 		shell.run('edit ' .. filename) | ||||
| 	end | ||||
|  | ||||
| 	return Config.load(fname, data) | ||||
| end | ||||
|  | ||||
| function Config.update(fname, data) | ||||
| 	local filename = 'usr/config/' .. fname | ||||
| 	Util.writeTable(filename, data) | ||||
|   | ||||
| @@ -41,9 +41,9 @@ end | ||||
|  | ||||
| function Entry:updateScroll() | ||||
| 	local ps = self.scroll | ||||
| 	local value = _val(self.value) | ||||
| 	if self.pos > #value then | ||||
| 		self.pos = #value | ||||
| 	local len = #_val(self.value) | ||||
| 	if self.pos > len then | ||||
| 		self.pos = len | ||||
| 		self.scroll = 0 -- ?? | ||||
| 	end | ||||
| 	if self.pos - self.scroll > self.width then | ||||
| @@ -51,6 +51,11 @@ function Entry:updateScroll() | ||||
| 	elseif self.pos < self.scroll then | ||||
| 		self.scroll = self.pos | ||||
| 	end | ||||
| 	if self.scroll > 0 then | ||||
| 		if self.scroll + self.width  > len then | ||||
| 			self.scroll = len - self.width | ||||
| 		end | ||||
| 	end | ||||
| 	if ps ~= self.scroll then | ||||
| 		self.textChanged = true | ||||
| 	end | ||||
| @@ -217,6 +222,10 @@ function Entry:paste(ie) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Entry:forcePaste() | ||||
| 	os.queueEvent('clipboard_paste') | ||||
| end | ||||
|  | ||||
| function Entry:clearLine() | ||||
| 	if #_val(self.value) > 0 then | ||||
| 		self:reset() | ||||
| @@ -363,9 +372,10 @@ local mappings = { | ||||
| 	--[ 'control-d'           ] = Entry.cutNextWord, | ||||
| 	[ 'control-x'           ] = Entry.cut, | ||||
| 	[ 'paste'               ] = Entry.paste, | ||||
| --	[ 'control-y'           ] = Entry.paste,  -- well this won't work... | ||||
| 	[ 'control-y'           ] = Entry.forcePaste,  -- well this won't work... | ||||
|  | ||||
| 	[ 'mouse_doubleclick'   ] = Entry.markWord, | ||||
| 	[ 'mouse_tripleclick'   ] = Entry.markAll, | ||||
| 	[ 'shift-left'          ] = Entry.markLeft, | ||||
| 	[ 'shift-right'         ] = Entry.markRight, | ||||
| 	[ 'mouse_down'          ] = Entry.markAnchor, | ||||
|   | ||||
							
								
								
									
										21
									
								
								sys/modules/opus/fuzzy.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								sys/modules/opus/fuzzy.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| -- Based on Squid's fuzzy search | ||||
| -- https://github.com/SquidDev-CC/artist/blob/vnext/artist/lib/match.lua | ||||
| -- | ||||
| -- not very fuzzy anymore | ||||
|  | ||||
| local SCORE_WEIGHT               = 1000 | ||||
| local LEADING_LETTER_PENALTY     = -30 | ||||
| local LEADING_LETTER_PENALTY_MAX = -90 | ||||
|  | ||||
| local _find = string.find | ||||
| local _max  = math.max | ||||
|  | ||||
| return function(str, pattern) | ||||
| 	local start = _find(str, pattern, 1, true) | ||||
| 	if start then | ||||
| 		-- All letters before the current one are considered leading, so add them to our penalty | ||||
| 		return SCORE_WEIGHT | ||||
| 			+ _max(LEADING_LETTER_PENALTY * (start - 1), LEADING_LETTER_PENALTY_MAX) | ||||
| 			- (#str - #pattern) | ||||
| 	end | ||||
| end | ||||
| @@ -24,9 +24,9 @@ function git.list(repository) | ||||
|  | ||||
| 	local function getContents() | ||||
| 		local dataUrl = string.format(TREE_URL, user, repo, branch) | ||||
| 		local contents, msg = Util.httpGet(dataUrl,TREE_HEADERS) | ||||
| 		local contents, msg = Util.httpGet(dataUrl, TREE_HEADERS) | ||||
| 		if not contents then | ||||
| 			error(_sformat('Failed to download %s\n%s', dataUrl, msg), 2) | ||||
| 			error(string.format('Failed to download %s\n%s', dataUrl, msg), 2) | ||||
| 		else | ||||
| 			return json.decode(contents) | ||||
| 		end | ||||
|   | ||||
| @@ -65,7 +65,7 @@ function GPS.locate(timeout, debug) | ||||
| 	if debug then | ||||
| 		print("Position is "..pos.x..","..pos.y..","..pos.z) | ||||
| 	end | ||||
| 	return vector.new(pos.x, pos.y, pos.z) | ||||
| 	return pos and vector.new(pos.x, pos.y, pos.z) | ||||
| end | ||||
|  | ||||
| function GPS.isAvailable() | ||||
|   | ||||
| @@ -50,18 +50,20 @@ function input:toCode(ch, code) | ||||
| 	   table.insert(result, 'alt') | ||||
| 	end | ||||
|  | ||||
| 	if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or | ||||
| 		 code == keys.leftShift or code == keys.rightShift then | ||||
| 		if code and modifiers[code] then | ||||
| 			table.insert(result, 'shift') | ||||
| 		elseif #ch == 1 then | ||||
| 			table.insert(result, ch:upper()) | ||||
| 		else | ||||
| 			table.insert(result, 'shift') | ||||
| 	if ch then -- some weird things happen with control/command on mac | ||||
| 		if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or | ||||
| 			code == keys.leftShift or code == keys.rightShift then | ||||
| 			if code and modifiers[code] then | ||||
| 				table.insert(result, 'shift') | ||||
| 			elseif #ch == 1 then | ||||
| 				table.insert(result, ch:upper()) | ||||
| 			else | ||||
| 				table.insert(result, 'shift') | ||||
| 				table.insert(result, ch) | ||||
| 			end | ||||
| 		elseif not code or not modifiers[code] then | ||||
| 			table.insert(result, ch) | ||||
| 		end | ||||
| 	elseif not code or not modifiers[code] then | ||||
| 		table.insert(result, ch) | ||||
| 	end | ||||
|  | ||||
| 	return table.concat(result, '-') | ||||
| @@ -118,6 +120,7 @@ function input:translate(event, code, p1, p2) | ||||
| 		local buttons = { 'mouse_click', 'mouse_rightclick' } | ||||
| 		self.mch = buttons[code] | ||||
| 		self.mfired = nil | ||||
| 		self.anchor = { x = p1, y = p2 } | ||||
| 		return { | ||||
| 			code = input:toCode('mouse_down', 255), | ||||
| 			button = code, | ||||
| @@ -132,6 +135,8 @@ function input:translate(event, code, p1, p2) | ||||
| 			button = code, | ||||
| 			x = p1, | ||||
| 			y = p2, | ||||
| 			dx = p1 - self.anchor.x, | ||||
| 			dy = p2 - self.anchor.y, | ||||
| 		} | ||||
|  | ||||
| 	elseif event == 'mouse_up' then | ||||
| @@ -141,18 +146,26 @@ function input:translate(event, code, p1, p2) | ||||
| 				 p1 == self.x and p2 == self.y and | ||||
| 				 (clock - self.timer < .5) then | ||||
|  | ||||
| 				self.mch = 'mouse_doubleclick' | ||||
| 				self.timer = nil | ||||
| 				self.clickCount = self.clickCount + 1 | ||||
| 				if self.clickCount == 3 then | ||||
| 					self.mch = 'mouse_tripleclick' | ||||
| 					self.timer = nil | ||||
| 					self.clickCount = 1 | ||||
| 				else | ||||
| 					self.mch = 'mouse_doubleclick' | ||||
| 				end | ||||
| 			else | ||||
| 				self.timer = os.clock() | ||||
| 				self.x = p1 | ||||
| 				self.y = p2 | ||||
| 				self.clickCount = 1 | ||||
| 			end | ||||
| 			self.mfired = input:toCode(self.mch, 255) | ||||
| 		else | ||||
| 			self.mch = 'mouse_up' | ||||
| 			self.mfired = input:toCode(self.mch, 255) | ||||
| 		end | ||||
|  | ||||
| 		return { | ||||
| 			code = self.mfired, | ||||
| 			button = code, | ||||
|   | ||||
| @@ -1,16 +1,19 @@ | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| local NFT = { } | ||||
|  | ||||
| -- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/ | ||||
|  | ||||
| local tColourLookup = { } | ||||
| local hexToColor = { } | ||||
| for n = 1, 16 do | ||||
| 	tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) | ||||
| 	hexToColor[string.sub("0123456789abcdef", n, n)] = 2 ^ (n - 1) | ||||
| end | ||||
| local colorToHex = Util.transpose(hexToColor) | ||||
|  | ||||
| local function getColourOf(hex) | ||||
| 	return tColourLookup[hex:byte()] | ||||
| 	return hexToColor[hex] | ||||
| end | ||||
|  | ||||
| function NFT.parse(imageText) | ||||
| @@ -62,8 +65,22 @@ function NFT.parse(imageText) | ||||
| 	return image | ||||
| end | ||||
|  | ||||
| function NFT.load(path) | ||||
| function NFT.transparency(image) | ||||
| 	for y = 1, image.height do | ||||
| 		for _,key in pairs(Util.keys(image.fg[y])) do | ||||
| 			if image.fg[y][key] == colors.magenta then | ||||
| 				image.fg[y][key] = nil | ||||
| 			end | ||||
| 		end | ||||
| 		for _,key in pairs(Util.keys(image.bg[y])) do | ||||
| 			if image.bg[y][key] == colors.magenta then | ||||
| 				image.bg[y][key] = nil | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function NFT.load(path) | ||||
| 	local imageText = Util.readFile(path) | ||||
| 	if not imageText then | ||||
| 		error('Unable to read image file') | ||||
| @@ -71,4 +88,35 @@ function NFT.load(path) | ||||
| 	return NFT.parse(imageText) | ||||
| end | ||||
|  | ||||
| function NFT.save(image, filename) | ||||
| 	local bgcode, txcode = '\30', '\31' | ||||
| 	local output = { } | ||||
|  | ||||
| 	for y = 1, image.height do | ||||
| 		local lastBG, lastFG | ||||
| 		if image.text[y] then | ||||
| 			for x = 1, #image.text[y] do | ||||
| 				local bg = image.bg[y][x] or colors.magenta | ||||
| 				if bg ~= lastBG then | ||||
| 					lastBG = bg | ||||
| 					table.insert(output, bgcode .. colorToHex[bg]) | ||||
| 				end | ||||
|  | ||||
| 				local fg = image.fg[y][x] or colors.magenta | ||||
| 				if fg ~= lastFG then | ||||
| 					lastFG = fg | ||||
| 					table.insert(output, txcode .. colorToHex[fg]) | ||||
| 				end | ||||
|  | ||||
| 				table.insert(output, image.text[y][x]) | ||||
| 			end | ||||
| 		end | ||||
|  | ||||
| 		if y < image.height then | ||||
| 			table.insert(output, '\n') | ||||
| 		end | ||||
| 	end | ||||
| 	Util.writeFile(filename, table.concat(output)) | ||||
| end | ||||
|  | ||||
| return NFT | ||||
|   | ||||
| @@ -36,61 +36,66 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	local maxScroll = 100 | ||||
| 	local cx, cy = 1, 1 | ||||
| 	local blink = false | ||||
| 	local bg, fg = parent.getBackgroundColor(), parent.getTextColor() | ||||
| 	local _bg, _fg = parent.getBackgroundColor(), parent.getTextColor() | ||||
|  | ||||
| 	local canvas = Canvas({ | ||||
| 	win.canvas = Canvas({ | ||||
| 		x       = sx, | ||||
| 		y       = sy, | ||||
| 		width   = w, | ||||
| 		height  = h, | ||||
| 		isColor = parent.isColor(), | ||||
| 		offy    = 0, | ||||
| 		bg      = _bg, | ||||
| 		fg      = _fg, | ||||
| 	}) | ||||
|  | ||||
| 	win.canvas = canvas | ||||
|  | ||||
| 	local function update() | ||||
| 		if isVisible then | ||||
| 			canvas:render(parent) | ||||
| 			win.canvas:render(parent) | ||||
| 			win.setCursorPos(cx, cy) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	local function scrollTo(y) | ||||
| 		y = math.max(0, y) | ||||
| 		y = math.min(#canvas.lines - canvas.height, y) | ||||
| 		y = math.min(#win.canvas.lines - win.canvas.height, y) | ||||
|  | ||||
| 		if y ~= canvas.offy then | ||||
| 			canvas.offy = y | ||||
| 			canvas:dirty() | ||||
| 		if y ~= win.canvas.offy then | ||||
| 			win.canvas.offy = y | ||||
| 			win.canvas:dirty() | ||||
| 			update() | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.write(str) | ||||
| 		str = tostring(str) or '' | ||||
| 		canvas:write(cx, cy + canvas.offy, str, bg, fg) | ||||
| 		win.canvas:write(cx, cy +  win.canvas.offy, str, win.canvas.bg, win.canvas.fg) | ||||
| 		win.setCursorPos(cx + #str, cy) | ||||
| 		update() | ||||
| 	end | ||||
|  | ||||
| 	function win.blit(str, fg, bg) | ||||
| 		canvas:blit(cx, cy + canvas.offy, str, bg, fg) | ||||
| 		win.canvas:blit(cx, cy + win.canvas.offy, str, bg, fg) | ||||
| 		win.setCursorPos(cx + #str, cy) | ||||
| 		update() | ||||
| 	end | ||||
|  | ||||
| 	function win.clear() | ||||
| 		canvas.offy = 0 | ||||
| 		for i = #canvas.lines, canvas.height + 1, -1 do | ||||
| 			canvas.lines[i] = nil | ||||
| 		win.canvas.offy = 0 | ||||
| 		for i = #win.canvas.lines, win.canvas.height + 1, -1 do | ||||
| 			win.canvas.lines[i] = nil | ||||
| 		end | ||||
| 		canvas:clear(bg, fg) | ||||
| 		win.canvas:clear() | ||||
| 		update() | ||||
| 	end | ||||
|  | ||||
| 	function win.getLine(n) | ||||
| 		local line = win.canvas.lines[n] | ||||
| 		return line.text, line.fg, line.bg | ||||
| 	end | ||||
|  | ||||
| 	function win.clearLine() | ||||
| 		canvas:clearLine(cy + canvas.offy, bg, fg) | ||||
| 		win.canvas:clearLine(cy + win.canvas.offy) | ||||
| 		win.setCursorPos(cx, cy) | ||||
| 		update() | ||||
| 	end | ||||
| @@ -102,10 +107,14 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	function win.setCursorPos(x, y) | ||||
| 		cx, cy = math.floor(x), math.floor(y) | ||||
| 		if isVisible then | ||||
| 			parent.setCursorPos(cx + canvas.x - 1, cy + canvas.y - 1) | ||||
| 			parent.setCursorPos(cx + win.canvas.x - 1, cy + win.canvas.y - 1) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.getCursorBlink() | ||||
| 		return blink | ||||
| 	end | ||||
|  | ||||
| 	function win.setCursorBlink(b) | ||||
| 		blink = b | ||||
| 		if isVisible then | ||||
| @@ -114,12 +123,12 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	end | ||||
|  | ||||
| 	function win.isColor() | ||||
| 		return canvas.isColor | ||||
| 		return win.canvas.isColor | ||||
| 	end | ||||
| 	win.isColour = win.isColor | ||||
|  | ||||
| 	function win.setTextColor(c) | ||||
| 		fg = c | ||||
| 		win.canvas.fg = c | ||||
| 	end | ||||
| 	win.setTextColour = win.setTextColor | ||||
|  | ||||
| @@ -139,38 +148,38 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	win.setPaletteColour = win.setPaletteColor | ||||
|  | ||||
| 	function win.setBackgroundColor(c) | ||||
| 		bg = c | ||||
| 		win.canvas.bg = c | ||||
| 	end | ||||
| 	win.setBackgroundColour = win.setBackgroundColor | ||||
|  | ||||
| 	function win.getSize() | ||||
| 		return canvas.width, canvas.height | ||||
| 		return win.canvas.width, win.canvas.height | ||||
| 	end | ||||
|  | ||||
| 	function win.scroll(n) | ||||
| 		n = n or 1 | ||||
| 		if n > 0 then | ||||
| 			local lines = #canvas.lines | ||||
| 			local lines = #win.canvas.lines | ||||
| 			for i = 1, n do | ||||
| 				canvas.lines[lines + i] = { } | ||||
| 				canvas:clearLine(lines + i, bg, fg) | ||||
| 				win.canvas.lines[lines + i] = { } | ||||
| 				win.canvas:clearLine(lines + i) | ||||
| 			end | ||||
| 			while #canvas.lines > maxScroll do | ||||
| 				table.remove(canvas.lines, 1) | ||||
| 			while #win.canvas.lines > maxScroll do | ||||
| 				table.remove(win.canvas.lines, 1) | ||||
| 			end | ||||
| 			scrollTo(#canvas.lines) | ||||
| 			canvas:dirty() | ||||
| 			scrollTo(#win.canvas.lines) | ||||
| 			win.canvas:dirty() | ||||
| 			update() | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.getTextColor() | ||||
| 		return fg | ||||
| 		return win.canvas.fg | ||||
| 	end | ||||
| 	win.getTextColour = win.getTextColor | ||||
|  | ||||
| 	function win.getBackgroundColor() | ||||
| 		return bg | ||||
| 		return win.canvas.bg | ||||
| 	end | ||||
| 	win.getBackgroundColour = win.getBackgroundColor | ||||
|  | ||||
| @@ -178,7 +187,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 		if visible ~= isVisible then | ||||
| 			isVisible = visible | ||||
| 			if isVisible then | ||||
| 				canvas:dirty() | ||||
| 				win.canvas:dirty() | ||||
| 				update() | ||||
| 			end | ||||
| 		end | ||||
| @@ -186,7 +195,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
|  | ||||
| 	function win.redraw() | ||||
| 		if isVisible then | ||||
| 			canvas:dirty() | ||||
| 			win.canvas:dirty() | ||||
| 			update() | ||||
| 		end | ||||
| 	end | ||||
| @@ -194,27 +203,27 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	function win.restoreCursor() | ||||
| 		if isVisible then | ||||
| 			win.setCursorPos(cx, cy) | ||||
| 			win.setTextColor(fg) | ||||
| 			win.setTextColor(win.canvas.fg) | ||||
| 			win.setCursorBlink(blink) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	function win.getPosition() | ||||
| 		return canvas.x, canvas.y | ||||
| 		return win.canvas.x, win.canvas.y | ||||
| 	end | ||||
|  | ||||
| 	function win.reposition(x, y, width, height) | ||||
| 		canvas.x, canvas.y = x, y | ||||
| 		canvas:resize(width or canvas.width, height or canvas.height) | ||||
| 		win.canvas.x, win.canvas.y = x, y | ||||
| 		win.canvas:resize(width or win.canvas.width, height or win.canvas.height) | ||||
| 	end | ||||
|  | ||||
| 	--[[ Additional methods ]]-- | ||||
| 	function win.scrollDown() | ||||
| 		scrollTo(canvas.offy + 1) | ||||
| 		scrollTo(win.canvas.offy + 1) | ||||
| 	end | ||||
|  | ||||
| 	function win.scrollUp() | ||||
| 		scrollTo(canvas.offy - 1) | ||||
| 		scrollTo(win.canvas.offy - 1) | ||||
| 	end | ||||
|  | ||||
| 	function win.scrollTop() | ||||
| @@ -222,7 +231,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	end | ||||
|  | ||||
| 	function win.scrollBottom() | ||||
| 		scrollTo(#canvas.lines) | ||||
| 		scrollTo(#win.canvas.lines) | ||||
| 	end | ||||
|  | ||||
| 	function win.setMaxScroll(ms) | ||||
| @@ -230,37 +239,35 @@ function Terminal.window(parent, sx, sy, w, h, isVisible) | ||||
| 	end | ||||
|  | ||||
| 	function win.getCanvas() | ||||
| 		return canvas | ||||
| 		return win.canvas | ||||
| 	end | ||||
|  | ||||
| 	function win.getParent() | ||||
| 		return parent | ||||
| 	end | ||||
|  | ||||
| 	canvas:clear() | ||||
| 	win.canvas:clear() | ||||
|  | ||||
| 	return win | ||||
| end | ||||
|  | ||||
| -- get windows contents | ||||
| function Terminal.getContents(win, parent) | ||||
| 	local oblit, oscp = parent.blit, parent.setCursorPos | ||||
| 	local lines = { } | ||||
| function Terminal.getContents(win) | ||||
| 	if not win.getLine then | ||||
| 		error('window is required') | ||||
| 	end | ||||
|  | ||||
| 	parent.blit = function(text, fg, bg) | ||||
| 		lines[#lines + 1] = { | ||||
| 	local lines = { } | ||||
| 	local _, h = win.getSize() | ||||
|  | ||||
| 	for i = 1, h do | ||||
| 		local text, fg, bg = win.getLine(i) | ||||
| 		lines[i] = { | ||||
| 			text = text, | ||||
| 			fg = fg, | ||||
| 			bg = bg, | ||||
| 		} | ||||
| 	end | ||||
| 	parent.setCursorPos = function() end | ||||
|  | ||||
| 	win.setVisible(true) | ||||
| 	win.redraw() | ||||
|  | ||||
| 	parent.blit = oblit | ||||
| 	parent.setCursorPos = oscp | ||||
|  | ||||
| 	return lines | ||||
| end | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| local Array      = require('opus.array') | ||||
| local Blit       = require('opus.ui.blit') | ||||
| local Canvas     = require('opus.ui.canvas') | ||||
| local class      = require('opus.class') | ||||
| local Event      = require('opus.event') | ||||
| local Input      = require('opus.input') | ||||
| @@ -5,7 +8,6 @@ local Transition = require('opus.ui.transition') | ||||
| local Util       = require('opus.util') | ||||
|  | ||||
| local _rep       = string.rep | ||||
| local _sub       = string.sub | ||||
| local colors     = _G.colors | ||||
| local device     = _G.device | ||||
| local fs         = _G.fs | ||||
| @@ -32,11 +34,16 @@ local textutils  = _G.textutils | ||||
| ]] | ||||
|  | ||||
| --[[-- Top Level Manager --]]-- | ||||
| local Manager = class() | ||||
| function Manager:init() | ||||
| local UI = { } | ||||
| function UI:init() | ||||
| 	self.devices = { } | ||||
| 	self.theme = { } | ||||
| 	self.extChars = Util.getVersion() >= 1.76 | ||||
| 	self.colors = { | ||||
| 		primary = colors.green, | ||||
| 		secondary = colors.lightGray, | ||||
| 		tertiary = colors.gray, | ||||
| 	} | ||||
|  | ||||
| 	local function keyFunction(event, code, held) | ||||
| 		local ie = Input:translate(event, code, held) | ||||
| @@ -44,8 +51,7 @@ function Manager:init() | ||||
| 		local currentPage = self:getActivePage() | ||||
| 		if ie and currentPage then | ||||
| 			local target = currentPage.focused or currentPage | ||||
| 			self:inputEvent(target, | ||||
| 				{ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie }) | ||||
| 			target:emit({ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie }) | ||||
| 			currentPage:sync() | ||||
| 		end | ||||
| 	end | ||||
| @@ -53,10 +59,7 @@ function Manager:init() | ||||
| 	local function resize(_, side) | ||||
| 		local dev = self.devices[side or 'terminal'] | ||||
| 		if dev and dev.currentPage then | ||||
| 			-- the parent doesn't have any children set... | ||||
| 			-- that's why we have to resize both the parent and the current page | ||||
| 			-- kinda makes sense | ||||
| 			dev.currentPage.parent:resize() | ||||
| 			dev:resize() | ||||
|  | ||||
| 			dev.currentPage:resize() | ||||
| 			dev.currentPage:draw() | ||||
| @@ -72,17 +75,14 @@ function Manager:init() | ||||
| 		monitor_resize = resize, | ||||
|  | ||||
| 		mouse_scroll = function(_, direction, x, y) | ||||
| 			local ie = Input:translate('mouse_scroll', direction, x, y) | ||||
|  | ||||
| 			local currentPage = self:getActivePage() | ||||
| 			if currentPage then | ||||
| 				local event = currentPage:pointToChild(x, y) | ||||
| 				local directions = { | ||||
| 					[ -1 ] = 'up', | ||||
| 					[  1 ] = 'down' | ||||
| 				} | ||||
| 				-- revisit - should send out scroll_up and scroll_down events | ||||
| 				-- let the element convert them to up / down | ||||
| 				self:inputEvent(event.element, | ||||
| 					{ type = 'key', key = directions[direction] }) | ||||
| 				event.type = ie.code | ||||
| 				event.ie = { code = ie.code, x = event.x, y = event.y } | ||||
| 				event.element:emit(event) | ||||
| 				currentPage:sync() | ||||
| 			end | ||||
| 		end, | ||||
| @@ -92,7 +92,7 @@ function Manager:init() | ||||
| 			if dev and dev.currentPage then | ||||
| 				Input:translate('mouse_click', 1, x, y) | ||||
| 				local ie = Input:translate('mouse_up', 1, x, y) | ||||
| 				self:click(dev.currentPage, ie.code, 1, x, y) | ||||
| 				self:click(dev.currentPage, ie) | ||||
| 			end | ||||
| 		end, | ||||
|  | ||||
| @@ -107,7 +107,7 @@ function Manager:init() | ||||
| 						currentPage:setFocus(event.element) | ||||
| 						currentPage:sync() | ||||
| 					end | ||||
| 					self:click(currentPage, ie.code, button, x, y) | ||||
| 					self:click(currentPage, ie) | ||||
| 				end | ||||
| 			end | ||||
| 		end, | ||||
| @@ -125,7 +125,7 @@ function Manager:init() | ||||
|  | ||||
| 			elseif ie and currentPage then | ||||
| 				if not currentPage.parent.device.side then | ||||
| 					self:click(currentPage, ie.code, button, x, y) | ||||
| 					self:click(currentPage, ie) | ||||
| 				end | ||||
| 			end | ||||
| 		end, | ||||
| @@ -135,7 +135,7 @@ function Manager:init() | ||||
| 			local currentPage = self:getActivePage() | ||||
|  | ||||
| 			if ie and currentPage then | ||||
| 				self:click(currentPage, ie.code, button, x, y) | ||||
| 				self:click(currentPage, ie) | ||||
| 			end | ||||
| 		end, | ||||
|  | ||||
| @@ -156,7 +156,7 @@ function Manager:init() | ||||
| 		end) | ||||
| end | ||||
|  | ||||
| function Manager:configure(appName, ...) | ||||
| function UI:configure(appName, ...) | ||||
| 	local defaults = Util.loadTable('usr/config/' .. appName) or { } | ||||
| 	if not defaults.device then | ||||
| 		defaults.device = { } | ||||
| @@ -190,19 +190,15 @@ function Manager:configure(appName, ...) | ||||
| 	end | ||||
|  | ||||
| 	if defaults.theme then | ||||
| 		for k,v in pairs(defaults.theme) do | ||||
| 			if self[k] and self[k].defaults then | ||||
| 				Util.merge(self[k].defaults, v) | ||||
| 			end | ||||
| 		end | ||||
| 		Util.deepMerge(self.theme, defaults.theme) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:disableEffects() | ||||
| 	self.defaultDevice.effectsEnabled = false | ||||
| function UI:disableEffects() | ||||
| 	self.term.effectsEnabled = false | ||||
| end | ||||
|  | ||||
| function Manager:loadTheme(filename) | ||||
| function UI:loadTheme(filename) | ||||
| 	if fs.exists(filename) then | ||||
| 		local theme, err = Util.loadTable(filename) | ||||
| 		if not theme then | ||||
| @@ -212,8 +208,20 @@ function Manager:loadTheme(filename) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:generateTheme(filename) | ||||
| function UI:generateTheme(filename) | ||||
| 	local t = { } | ||||
|  | ||||
| 	local function getName(d) | ||||
| 		if type(d) == 'string' then | ||||
| 			return string.format("'%s'", d) | ||||
| 		end | ||||
| 		for c, n in pairs(colors) do | ||||
| 			if n == d then | ||||
| 				return 'colors.' .. c | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	for k,v in pairs(self) do | ||||
| 		if type(v) == 'table' then | ||||
| 			if v._preload then | ||||
| @@ -226,72 +234,96 @@ function Manager:generateTheme(filename) | ||||
| 						if not t[k] then | ||||
| 							t[k] = { } | ||||
| 						end | ||||
| 						for c, n in pairs(colors) do | ||||
| 							if n == d then | ||||
| 								t[k][p] = 'colors.' .. c | ||||
| 								break | ||||
| 							end | ||||
| 						end | ||||
| 						t[k][p] = getName(d) | ||||
| 					end | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	t.colors = { | ||||
| 		primary = getName(self.colors.primary), | ||||
| 		secondary = getName(self.colors.secondary), | ||||
| 		tertiary = getName(self.colors.tertiary), | ||||
| 	} | ||||
| 	Util.writeFile(filename, textutils.serialize(t):gsub('(")', '')) | ||||
| end | ||||
|  | ||||
| function Manager:emitEvent(event) | ||||
| function UI:emitEvent(event) | ||||
| 	local currentPage = self:getActivePage() | ||||
| 	if currentPage and currentPage.focused then | ||||
| 		return currentPage.focused:emit(event) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:inputEvent(parent, event) -- deprecate ? | ||||
| 	return parent and parent:emit(event) | ||||
| end | ||||
| function UI:click(target, ie) | ||||
| 	local clickEvent | ||||
|  | ||||
| function Manager:click(target, code, button, x, y) | ||||
| 	local clickEvent = target:pointToChild(x, y) | ||||
| 	if ie.code == 'mouse_drag' then | ||||
| 		local function getPosition(element, x, y) | ||||
| 			repeat | ||||
| 				x = x - element.x + 1 | ||||
| 				y = y - element.y + 1 | ||||
| 				element = element.parent | ||||
| 			until not element | ||||
| 			return x, y | ||||
| 		end | ||||
|  | ||||
| 	if code == 'mouse_doubleclick' then | ||||
| 		if self.doubleClickElement ~= clickEvent.element then | ||||
| 		local x, y = getPosition(self.lastClicked, ie.x, ie.y) | ||||
|  | ||||
| 		clickEvent = { | ||||
| 			element = self.lastClicked, | ||||
| 			x = x, | ||||
| 			y = y, | ||||
| 			dx = ie.dx, | ||||
| 			dy = ie.dy, | ||||
| 		} | ||||
| 	else | ||||
| 		clickEvent = target:pointToChild(ie.x, ie.y) | ||||
| 	end | ||||
|  | ||||
| 	-- hack for dropdown menus | ||||
| 	if ie.code == 'mouse_click' and not clickEvent.element.focus then | ||||
| 		self:emitEvent({ type = 'mouse_out' }) | ||||
| 	end | ||||
|  | ||||
| 	if ie.code == 'mouse_doubleclick' then | ||||
| 		if self.lastClicked ~= clickEvent.element then | ||||
| 			return | ||||
| 		end | ||||
| 	else | ||||
| 		self.doubleClickElement = clickEvent.element | ||||
| 		self.lastClicked = clickEvent.element | ||||
| 	end | ||||
|  | ||||
| 	clickEvent.button = button | ||||
| 	clickEvent.type = code | ||||
| 	clickEvent.key = code | ||||
| 	clickEvent.ie = { code = code, x = clickEvent.x, y = clickEvent.y } | ||||
| 	clickEvent.button = ie.button | ||||
| 	clickEvent.type = ie.code | ||||
| 	clickEvent.key = ie.code | ||||
| 	clickEvent.ie = { code = ie.code, x = clickEvent.x, y = clickEvent.y } | ||||
| 	clickEvent.raw = ie | ||||
|  | ||||
| 	if clickEvent.element.focus then | ||||
| 		target:setFocus(clickEvent.element) | ||||
| 	end | ||||
| 	self:inputEvent(clickEvent.element, clickEvent) | ||||
| 	clickEvent.element:emit(clickEvent) | ||||
|  | ||||
| 	target:sync() | ||||
| end | ||||
|  | ||||
| function Manager:setDefaultDevice(dev) | ||||
| 	self.defaultDevice = dev | ||||
| function UI:setDefaultDevice(dev) | ||||
| 	self.term = dev | ||||
| end | ||||
|  | ||||
| function Manager:addPage(name, page) | ||||
| function UI:addPage(name, page) | ||||
| 	if not self.pages then | ||||
| 		self.pages = { } | ||||
| 	end | ||||
| 	self.pages[name] = page | ||||
| end | ||||
|  | ||||
| function Manager:setPages(pages) | ||||
| function UI:setPages(pages) | ||||
| 	self.pages = pages | ||||
| end | ||||
|  | ||||
| function Manager:getPage(pageName) | ||||
| function UI:getPage(pageName) | ||||
| 	local page = self.pages[pageName] | ||||
|  | ||||
| 	if not page then | ||||
| @@ -301,19 +333,18 @@ function Manager:getPage(pageName) | ||||
| 	return page | ||||
| end | ||||
|  | ||||
| function Manager:getActivePage(page) | ||||
| function UI:getActivePage(page) | ||||
| 	if page then | ||||
| 		return page.parent.currentPage | ||||
| 	end | ||||
| 	return self.defaultDevice.currentPage | ||||
| 	return self.term.currentPage | ||||
| end | ||||
|  | ||||
| function Manager:setActivePage(page) | ||||
| function UI:setActivePage(page) | ||||
| 	page.parent.currentPage = page | ||||
| 	page.parent.canvas = page.canvas | ||||
| end | ||||
|  | ||||
| function Manager:setPage(pageOrName, ...) | ||||
| function UI:setPage(pageOrName, ...) | ||||
| 	local page = pageOrName | ||||
|  | ||||
| 	if type(pageOrName) == 'string' then | ||||
| @@ -333,7 +364,6 @@ function Manager:setPage(pageOrName, ...) | ||||
| 			page.previousPage = currentPage | ||||
| 		end | ||||
| 		self:setActivePage(page) | ||||
| 		--page:clear(page.backgroundColor) | ||||
| 		page:enable(...) | ||||
| 		page:draw() | ||||
| 		if page.focused then | ||||
| @@ -344,27 +374,27 @@ function Manager:setPage(pageOrName, ...) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:getCurrentPage() | ||||
| 	return self.defaultDevice.currentPage | ||||
| function UI:getCurrentPage() | ||||
| 	return self.term.currentPage | ||||
| end | ||||
|  | ||||
| function Manager:setPreviousPage() | ||||
| 	if self.defaultDevice.currentPage.previousPage then | ||||
| 		local previousPage = self.defaultDevice.currentPage.previousPage.previousPage | ||||
| 		self:setPage(self.defaultDevice.currentPage.previousPage) | ||||
| 		self.defaultDevice.currentPage.previousPage = previousPage | ||||
| function UI:setPreviousPage() | ||||
| 	if self.term.currentPage.previousPage then | ||||
| 		local previousPage = self.term.currentPage.previousPage.previousPage | ||||
| 		self:setPage(self.term.currentPage.previousPage) | ||||
| 		self.term.currentPage.previousPage = previousPage | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:getDefaults(element, args) | ||||
| function UI:getDefaults(element, args) | ||||
| 	local defaults = Util.deepCopy(element.defaults) | ||||
| 	if args then | ||||
| 		Manager:mergeProperties(defaults, args) | ||||
| 		UI:mergeProperties(defaults, args) | ||||
| 	end | ||||
| 	return defaults | ||||
| end | ||||
|  | ||||
| function Manager:mergeProperties(obj, args) | ||||
| function UI:mergeProperties(obj, args) | ||||
| 	if args then | ||||
| 		for k,v in pairs(args) do | ||||
| 			if k == 'accelerators' then | ||||
| @@ -380,7 +410,7 @@ function Manager:mergeProperties(obj, args) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Manager:pullEvents(...) | ||||
| function UI:pullEvents(...) | ||||
| 	local s, m = pcall(Event.pullEvents, ...) | ||||
| 	self.term:reset() | ||||
| 	if not s and m then | ||||
| @@ -388,21 +418,20 @@ function Manager:pullEvents(...) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| Manager.exitPullEvents = Event.exitPullEvents | ||||
| Manager.quit = Event.exitPullEvents | ||||
| Manager.start = Manager.pullEvents | ||||
| UI.exitPullEvents = Event.exitPullEvents | ||||
| UI.quit = Event.exitPullEvents | ||||
| UI.start = UI.pullEvents | ||||
|  | ||||
| local UI = Manager() | ||||
| UI:init() | ||||
|  | ||||
| --[[-- Basic drawable area --]]-- | ||||
| UI.Window = class() | ||||
| UI.Window = class(Canvas) | ||||
| UI.Window.uid = 1 | ||||
| UI.Window.docs = { } | ||||
| UI.Window.defaults = { | ||||
| 	UIElement = 'Window', | ||||
| 	x = 1, | ||||
| 	y = 1, | ||||
| 	-- z = 0, -- eventually... | ||||
| 	offx = 0, | ||||
| 	offy = 0, | ||||
| 	cursorX = 1, | ||||
| @@ -413,7 +442,9 @@ function UI.Window:init(args) | ||||
| 	local defaults = args | ||||
| 	local m = getmetatable(self)  -- get the class for this instance | ||||
| 	repeat | ||||
| 		defaults = UI:getDefaults(m, defaults) | ||||
| 		if m ~= Canvas then | ||||
| 			defaults = UI:getDefaults(m, defaults) | ||||
| 		end | ||||
| 		m = m._base | ||||
| 	until not m | ||||
| 	UI:mergeProperties(self, defaults) | ||||
| @@ -438,6 +469,9 @@ function UI.Window:init(args) | ||||
| 	until not m | ||||
| end | ||||
|  | ||||
| UI.Window.docs.postInit = [[postInit(VOID) | ||||
| Called once the window has all the properties set. | ||||
| Override to calculate properties or to dynamically add children]] | ||||
| function UI.Window:postInit() | ||||
| 	if self.parent then | ||||
| 		-- this will cascade down the whole tree of elements starting at the | ||||
| @@ -531,47 +565,60 @@ function UI.Window:layout() | ||||
| 	if not self.height then | ||||
| 		self.height = self.parent.height - self.y + 1 | ||||
| 	end | ||||
|  | ||||
| 	self.width = math.max(self.width, 1) | ||||
| 	self.height = math.max(self.height, 1) | ||||
|  | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| end | ||||
|  | ||||
| -- Called when the window's parent has be assigned | ||||
| function UI.Window:setParent() | ||||
| 	self.oh, self.ow = self.height, self.width | ||||
| 	self.ox, self.oy = self.x, self.y | ||||
| 	self.oex, self.oey = self.ex, self.ey | ||||
|  | ||||
| 	self:layout() | ||||
|  | ||||
| 	-- Experimental | ||||
| 	-- Inherit properties from the parent container | ||||
| 	-- does this need to be in reverse order ? | ||||
| 	local m = getmetatable(self)  -- get the class for this instance | ||||
| 	repeat | ||||
| 		if m.inherits then | ||||
| 			for k, v in pairs(m.inherits) do | ||||
| 				local value = self.parent:getProperty(v) | ||||
| 				if value then | ||||
| 					self[k] = value | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 		m = m._base | ||||
| 	until not m | ||||
|  | ||||
| 	self:initChildren() | ||||
| end | ||||
|  | ||||
| function UI.Window:resize() | ||||
| 	self.height, self.width = self.oh, self.ow | ||||
| 	self.x, self.y = self.ox, self.oy | ||||
| 	self.ex, self.ey = self.oex, self.oey | ||||
|  | ||||
| 	self:layout() | ||||
|  | ||||
| 	if self.children then | ||||
| 		for _,child in ipairs(self.children) do | ||||
| 		for child in self:eachChild() do | ||||
| 			child:resize() | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:reposition(x, y, w, h) | ||||
| 	if not self.lines then | ||||
| 		Canvas.init(self, { | ||||
| 			x = x, | ||||
| 			y = y, | ||||
| 			width = w, | ||||
| 			height = h, | ||||
| 			isColor = self.parent.isColor, | ||||
| 		}) | ||||
| 	else | ||||
| 		self:move(x, y) | ||||
| 		Canvas.resize(self, w, h) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| UI.Window.docs.raise = [[raise(VOID) | ||||
| Raise this window to the top]] | ||||
| function UI.Window:raise() | ||||
| 	Array.removeByValue(self.parent.children, self) | ||||
| 	table.insert(self.parent.children, self) | ||||
| 	self:dirty(true) | ||||
| end | ||||
|  | ||||
| UI.Window.docs.add = [[add(TABLE) | ||||
| Add element(s) to a window. Example: | ||||
| page:add({ | ||||
| @@ -584,6 +631,20 @@ function UI.Window:add(children) | ||||
| 	self:initChildren() | ||||
| end | ||||
|  | ||||
| function UI.Window:eachChild() | ||||
| 	local c = self.children and Util.shallowCopy(self.children) | ||||
| 	local i = 0 | ||||
| 	return function() | ||||
| 		i = i + 1 | ||||
| 		return c and c[i] | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:remove() | ||||
| 	Array.removeByValue(self.parent.children, self) | ||||
| 	self.parent:dirty(true) | ||||
| end | ||||
|  | ||||
| function UI.Window:getCursorPos() | ||||
| 	return self.cursorX, self.cursorY | ||||
| end | ||||
| @@ -595,18 +656,20 @@ function UI.Window:setCursorPos(x, y) | ||||
| end | ||||
|  | ||||
| function UI.Window:setCursorBlink(blink) | ||||
| 	self.parent:setCursorBlink(blink) | ||||
| 	self.cursorBlink = blink | ||||
| end | ||||
|  | ||||
| UI.Window.docs.draw = [[draw(VOID) | ||||
| Redraws the window in the internal buffer.]] | ||||
| function UI.Window:draw() | ||||
| 	self:clear(self.backgroundColor) | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 			if child.enabled then | ||||
| 				child:draw() | ||||
| 			end | ||||
| 	self:clear() | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.Window:drawChildren() | ||||
| 	for child in self:eachChild() do | ||||
| 		if child.enabled then | ||||
| 			child:draw() | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
| @@ -633,19 +696,38 @@ function UI.Window:sync() | ||||
| end | ||||
|  | ||||
| function UI.Window:enable(...) | ||||
| 	self.enabled = true | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 			child:enable(...) | ||||
| 	if not self.enabled then | ||||
| 		self.enabled = true | ||||
| 		if self.transitionHint then | ||||
| 			self:addTransition(self.transitionHint) | ||||
| 		end | ||||
|  | ||||
| 		if self.modal then | ||||
| 			self:raise() | ||||
| 			self:capture(self) | ||||
| 		end | ||||
|  | ||||
| 		for child in self:eachChild() do | ||||
| 			if not child.enabled then | ||||
| 				child:enable(...) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:disable() | ||||
| 	self.enabled = false | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 			child:disable() | ||||
| 	if self.enabled then | ||||
| 		self.enabled = false | ||||
| 		self.parent:dirty(true) | ||||
|  | ||||
| 		if self.modal then | ||||
| 			self:release(self) | ||||
| 		end | ||||
|  | ||||
| 		for child in self:eachChild() do | ||||
| 			if child.enabled then | ||||
| 				child:disable() | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
| @@ -656,13 +738,9 @@ function UI.Window:setTextScale(textScale) | ||||
| end | ||||
|  | ||||
| UI.Window.docs.clear = [[clear(opt COLOR bg, opt COLOR fg) | ||||
| Clears the window using the either the passed values or the defaults for that window.]] | ||||
| Clears the window using either the passed values or the defaults for that window.]] | ||||
| function UI.Window:clear(bg, fg) | ||||
| 	if self.canvas then | ||||
| 		self.canvas:clear(bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| 	else | ||||
| 		self:clearArea(1 + self.offx, 1 + self.offy, self.width, self.height, bg) | ||||
| 	end | ||||
| 	Canvas.clear(self, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| end | ||||
|  | ||||
| function UI.Window:clearLine(y, bg) | ||||
| @@ -670,39 +748,28 @@ function UI.Window:clearLine(y, bg) | ||||
| end | ||||
|  | ||||
| function UI.Window:clearArea(x, y, width, height, bg) | ||||
| 	self:fillArea(x, y, width, height, ' ', bg) | ||||
| end | ||||
|  | ||||
| function UI.Window:fillArea(x, y, width, height, fillChar, bg, fg) | ||||
| 	if width > 0 then | ||||
| 		local filler = _rep(' ', width) | ||||
| 		local filler = _rep(fillChar, width) | ||||
| 		for i = 0, height - 1 do | ||||
| 			self:write(x, y + i, filler, bg) | ||||
| 			self:write(x, y + i, filler, bg, fg) | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:write(x, y, text, bg, fg) | ||||
| 	bg = bg or self.backgroundColor | ||||
| 	fg = fg or self.textColor | ||||
|  | ||||
| 	if self.canvas then | ||||
| 		self.canvas:write(x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| 	else | ||||
| 		x = x - self.offx | ||||
| 		y = y - self.offy | ||||
| 		if y <= self.height and y > 0 then | ||||
| 			self.parent:write( | ||||
| 				self.x + x - 1, self.y + y - 1, tostring(text), bg, fg) | ||||
| 		end | ||||
| 	end | ||||
| 	Canvas.write(self, x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor')) | ||||
| end | ||||
|  | ||||
| function UI.Window:centeredWrite(y, text, bg, fg) | ||||
| 	if #text >= self.width then | ||||
| 		self:write(1, y, text, bg, fg) | ||||
| 	else | ||||
| 		local space = math.floor((self.width-#text) / 2) | ||||
| 		local filler = _rep(' ', space + 1) | ||||
| 		local str = _sub(filler, 1, space) .. text | ||||
| 		str = str .. _sub(filler, self.width - #str + 1) | ||||
| 		self:write(1, y, str, bg, fg) | ||||
| 		local x = math.floor((self.width-#text) / 2) + 1 | ||||
| 		self:write(x, y, text, bg, fg) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| @@ -710,89 +777,19 @@ function UI.Window:print(text, bg, fg) | ||||
| 	local marginLeft = self.marginLeft or 0 | ||||
| 	local marginRight = self.marginRight or 0 | ||||
| 	local width = self.width - marginLeft - marginRight | ||||
| 	local cs = { | ||||
| 		bg = bg or self:getProperty('backgroundColor'), | ||||
| 		fg = fg or self:getProperty('textColor'), | ||||
| 		palette = self.palette, | ||||
| 	} | ||||
|  | ||||
| 	local function nextWord(line, cx) | ||||
| 		local result = { line:find("(%w+)", cx) } | ||||
| 		if #result > 1 and result[2] > cx then | ||||
| 			return _sub(line, cx, result[2] + 1) | ||||
| 		elseif #result > 0 and result[1] == cx then | ||||
| 			result = { line:find("(%w+)", result[2]) } | ||||
| 			if #result > 0 then | ||||
| 				return _sub(line, cx, result[1] + 1) | ||||
| 			end | ||||
| 		end | ||||
| 		if cx <= #line then | ||||
| 			return _sub(line, cx, #line) | ||||
| 	local y = (self.marginTop or 0) + 1 | ||||
| 	for _,line in pairs(Util.split(text)) do | ||||
| 		for _, ln in ipairs(Blit(line, cs):wrap(width)) do | ||||
| 			self:blit(marginLeft + 1, y, ln.text, ln.bg, ln.fg) | ||||
| 			y = y + 1 | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	local function pieces(f, bg, fg) | ||||
| 		local pos = 1 | ||||
| 		local t = { } | ||||
| 		while true do | ||||
| 			local s = string.find(f, '\027', pos, true) | ||||
| 			if not s then | ||||
| 				break | ||||
| 			end | ||||
| 			if pos < s then | ||||
| 				table.insert(t, _sub(f, pos, s - 1)) | ||||
| 			end | ||||
| 			local seq = _sub(f, s) | ||||
| 			seq = seq:match("\027%[([%d;]+)m") | ||||
| 			local e = { } | ||||
| 			for color in string.gmatch(seq, "%d+") do | ||||
| 				color = tonumber(color) | ||||
| 				if color == 0 then | ||||
| 					e.fg = fg | ||||
| 					e.bg = bg | ||||
| 				elseif color > 20 then | ||||
| 					e.bg = 2 ^ (color - 21) | ||||
| 				else | ||||
| 					e.fg = 2 ^ (color - 1) | ||||
| 				end | ||||
| 			end | ||||
| 			table.insert(t, e) | ||||
| 			pos = s + #seq + 3 | ||||
| 		end | ||||
| 		if pos <= #f then | ||||
| 			table.insert(t, _sub(f, pos)) | ||||
| 		end | ||||
| 		return t | ||||
| 	end | ||||
|  | ||||
| 	local lines = Util.split(text) | ||||
| 	for k,line in pairs(lines) do | ||||
| 		local fragments = pieces(line, bg, fg) | ||||
| 		for _, fragment in ipairs(fragments) do | ||||
| 			local lx = 1 | ||||
| 			if type(fragment) == 'table' then -- ansi sequence | ||||
| 				fg = fragment.fg | ||||
| 				bg = fragment.bg | ||||
| 			else | ||||
| 				while true do | ||||
| 					local word = nextWord(fragment, lx) | ||||
| 					if not word then | ||||
| 						break | ||||
| 					end | ||||
| 					local w = word | ||||
| 					if self.cursorX + #word > width then | ||||
| 						self.cursorX = marginLeft + 1 | ||||
| 						self.cursorY = self.cursorY + 1 | ||||
| 						w = word:gsub('^ ', '') | ||||
| 					end | ||||
| 					self:write(self.cursorX, self.cursorY, w, bg, fg) | ||||
| 					self.cursorX = self.cursorX + #w | ||||
| 					lx = lx + #word | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 		if lines[k + 1] then | ||||
| 			self.cursorX = marginLeft + 1 | ||||
| 			self.cursorY = self.cursorY + 1 | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	return self.cursorX, self.cursorY | ||||
| end | ||||
|  | ||||
| UI.Window.docs.focus = [[focus(VOID) | ||||
| @@ -822,11 +819,11 @@ function UI.Window:release(child) | ||||
| end | ||||
|  | ||||
| function UI.Window:pointToChild(x, y) | ||||
| 	-- TODO: get rid of this offx/y mess and scroll canvas instead | ||||
| 	x = x + self.offx - self.x + 1 | ||||
| 	y = y + self.offy - self.y + 1 | ||||
| 	if self.children then | ||||
| 		for _,child in pairs(self.children) do | ||||
| 		for i = #self.children, 1, -1 do | ||||
| 			local child = self.children[i] | ||||
| 			if child.enabled and not child.inactive and | ||||
| 				 x >= child.x and x < child.x + child.width and | ||||
| 				 y >= child.y and y < child.y + child.height then | ||||
| @@ -868,7 +865,7 @@ function UI.Window:getFocusables() | ||||
| 	end | ||||
|  | ||||
| 	if self.children then | ||||
| 		getFocusable(self, self.x, self.y) | ||||
| 		getFocusable(self) | ||||
| 	end | ||||
|  | ||||
| 	return focusable | ||||
| @@ -882,36 +879,28 @@ function UI.Window:focusFirst() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:refocus() | ||||
| 	local el = self | ||||
| 	while el do | ||||
| 		local focusables = el:getFocusables() | ||||
| 		if focusables[1] then | ||||
| 			self:setFocus(focusables[1]) | ||||
| 			break | ||||
| 		end | ||||
| 		el = el.parent | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:scrollIntoView() | ||||
| 	local parent = self.parent | ||||
| 	local offx, offy = parent.offx, parent.offy | ||||
|  | ||||
| 	if self.x <= parent.offx then | ||||
| 		parent.offx = math.max(0, self.x - 1) | ||||
| 		parent:draw() | ||||
| 		if offx ~= parent.offx then | ||||
| 			parent:draw() | ||||
| 		end | ||||
| 	elseif self.x + self.width > parent.width + parent.offx then | ||||
| 		parent.offx = self.x + self.width - parent.width - 1 | ||||
| 		parent:draw() | ||||
| 		if offx ~= parent.offx then | ||||
| 			parent:draw() | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	-- TODO: fix | ||||
| 	local function setOffset(y) | ||||
| 		parent.offy = y | ||||
| 		if parent.canvas then | ||||
| 			parent.canvas.offy = parent.offy | ||||
| 		if offy ~= parent.offy then | ||||
| 			parent:draw() | ||||
| 		end | ||||
| 		parent:draw() | ||||
| 	end | ||||
|  | ||||
| 	if self.y <= parent.offy then | ||||
| @@ -921,45 +910,8 @@ function UI.Window:scrollIntoView() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Window:getCanvas() | ||||
| 	local el = self | ||||
| 	repeat | ||||
| 		if el.canvas then | ||||
| 			return el.canvas | ||||
| 		end | ||||
| 		el = el.parent | ||||
| 	until not el | ||||
| end | ||||
|  | ||||
| function UI.Window:addLayer(bg, fg) | ||||
| 	local canvas = self:getCanvas() | ||||
| 	local x, y = self.x, self.y | ||||
| 	local parent = self.parent | ||||
| 	while parent and not parent.canvas do | ||||
| 		x = x + parent.x - 1 | ||||
| 		y = y + parent.y - 1 | ||||
| 		parent = parent.parent | ||||
| 	end | ||||
| 	canvas = canvas:addLayer({ | ||||
| 		x = x, y = y, height = self.height, width = self.width | ||||
| 	}, bg, fg) | ||||
|  | ||||
| 	canvas:clear(bg or self.backgroundColor, fg or self.textColor) | ||||
| 	return canvas | ||||
| end | ||||
|  | ||||
| function UI.Window:addTransition(effect, args) | ||||
| 	if self.parent then | ||||
| 		args = args or { } | ||||
| 		if not args.x then -- not good | ||||
| 			args.x, args.y = self.x, self.y -- getPosition(self) | ||||
| 			args.width = self.width | ||||
| 			args.height = self.height | ||||
| 		end | ||||
|  | ||||
| 		args.canvas = args.canvas or self.canvas | ||||
| 		self.parent:addTransition(effect, args) | ||||
| 	end | ||||
| function UI.Window:addTransition(effect, args, canvas) | ||||
| 	self.parent:addTransition(effect, args, canvas or self) | ||||
| end | ||||
|  | ||||
| function UI.Window:emit(event) | ||||
| @@ -991,7 +943,16 @@ function UI.Window:getProperty(property) | ||||
| end | ||||
|  | ||||
| function UI.Window:find(uid) | ||||
| 	return self.children and Util.find(self.children, 'uid', uid) | ||||
| 	local el = self.children and Util.find(self.children, 'uid', uid) | ||||
| 	if not el then | ||||
| 		for child in self:eachChild() do | ||||
| 			el = child:find(uid) | ||||
| 			if el then | ||||
| 				break | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	return el | ||||
| end | ||||
|  | ||||
| function UI.Window:eventHandler() | ||||
| @@ -1010,18 +971,14 @@ UI.Device.defaults = { | ||||
| function UI.Device:postInit() | ||||
| 	self.device = self.device or term.current() | ||||
|  | ||||
| 	--if self.deviceType then | ||||
| 	--	self.device = device[self.deviceType] | ||||
| 	--end | ||||
|  | ||||
| 	if not self.device.setTextScale then | ||||
| 		self.device.setTextScale = function() end | ||||
| 	end | ||||
|  | ||||
| 	self.device.setTextScale(self.textScale) | ||||
| 	self.width, self.height = self.device.getSize() | ||||
|  | ||||
| 	self.isColor = self.device.isColor() | ||||
| 	Canvas.init(self, { isColor = self.isColor }) | ||||
|  | ||||
| 	UI.devices[self.device.side or 'terminal'] = self | ||||
| end | ||||
| @@ -1031,8 +988,8 @@ function UI.Device:resize() | ||||
| 	self.width, self.height = self.device.getSize() | ||||
| 	self.lines = { } | ||||
| 	-- TODO: resize all pages added to this device | ||||
| 	self.canvas:resize(self.width, self.height) | ||||
| 	self.canvas:clear(self.backgroundColor, self.textColor) | ||||
| 	Canvas.resize(self, self.width, self.height) | ||||
| 	Canvas.clear(self, self.backgroundColor, self.textColor) | ||||
| end | ||||
|  | ||||
| function UI.Device:setCursorPos(x, y) | ||||
| @@ -1046,7 +1003,6 @@ end | ||||
|  | ||||
| function UI.Device:setCursorBlink(blink) | ||||
| 	self.cursorBlink = blink | ||||
| 	self.device.setCursorBlink(blink) | ||||
| end | ||||
|  | ||||
| function UI.Device:setTextScale(textScale) | ||||
| @@ -1061,27 +1017,30 @@ function UI.Device:reset() | ||||
| 	self.device.setCursorPos(1, 1) | ||||
| end | ||||
|  | ||||
| function UI.Device:addTransition(effect, args) | ||||
| function UI.Device:addTransition(effect, args, canvas) | ||||
| 	if not self.transitions then | ||||
| 		self.transitions = { } | ||||
| 	end | ||||
|  | ||||
| 	args = args or { } | ||||
| 	args.ex = args.x + args.width - 1 | ||||
| 	args.ey = args.y + args.height - 1 | ||||
| 	args.canvas = args.canvas or self.canvas | ||||
|  | ||||
| 	if type(effect) == 'string' then | ||||
| 		effect = Transition[effect] | ||||
| 		if not effect then | ||||
| 			error('Invalid transition') | ||||
| 		effect = Transition[effect] or error('Invalid transition') | ||||
| 	end | ||||
|  | ||||
| 	-- there can be only one | ||||
| 	for k,v in pairs(self.transitions) do | ||||
| 		if v.canvas == canvas then | ||||
| 			table.remove(self.transitions, k) | ||||
| 			break | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	table.insert(self.transitions, { update = effect(args), args = args }) | ||||
| 	table.insert(self.transitions, { effect = effect, args = args or { }, canvas = canvas }) | ||||
| end | ||||
|  | ||||
| function UI.Device:runTransitions(transitions, canvas) | ||||
| function UI.Device:runTransitions(transitions) | ||||
| 	for _,k in pairs(transitions) do | ||||
| 		k.update = k.effect(k.canvas, k.args) | ||||
| 	end | ||||
| 	while true do | ||||
| 		for _,k in ipairs(Util.keys(transitions)) do | ||||
| 			local transition = transitions[k] | ||||
| @@ -1089,7 +1048,7 @@ function UI.Device:runTransitions(transitions, canvas) | ||||
| 				transitions[k] = nil | ||||
| 			end | ||||
| 		end | ||||
| 		canvas:render(self.device) | ||||
| 		self.currentPage:render(self, true) | ||||
| 		if Util.empty(transitions) then | ||||
| 			break | ||||
| 		end | ||||
| @@ -1101,17 +1060,19 @@ function UI.Device:sync() | ||||
| 	local transitions = self.effectsEnabled and self.transitions | ||||
| 	self.transitions = nil | ||||
|  | ||||
| 	if self:getCursorBlink() then | ||||
| 		self.device.setCursorBlink(false) | ||||
| 	end | ||||
| 	self.device.setCursorBlink(false) | ||||
|  | ||||
| 	self.canvas:render(self.device) | ||||
| 	if transitions then | ||||
| 		self:runTransitions(transitions, self.canvas) | ||||
| 		self:runTransitions(transitions) | ||||
| 	else | ||||
| 		self.currentPage:render(self, true) | ||||
| 	end | ||||
|  | ||||
| 	if self:getCursorBlink() then | ||||
| 		self.device.setCursorPos(self.cursorX, self.cursorY) | ||||
| 		if self.isColor then | ||||
| 			self.device.setTextColor(colors.orange) | ||||
| 		end | ||||
| 		self.device.setCursorBlink(true) | ||||
| 	end | ||||
| end | ||||
| @@ -1143,7 +1104,7 @@ local function loadComponents() | ||||
| 				return self(...) | ||||
| 			end | ||||
| 		}) | ||||
| 		UI[name]._preload = function(self) | ||||
| 		UI[name]._preload = function() | ||||
| 			return load(name) | ||||
| 		end | ||||
| 	end | ||||
| @@ -1152,6 +1113,12 @@ end | ||||
| loadComponents() | ||||
| UI:loadTheme('usr/config/ui.theme') | ||||
| Util.merge(UI.Window.defaults, UI.theme.Window) | ||||
| UI:setDefaultDevice(UI.Device({ device = term.current() })) | ||||
| Util.merge(UI.colors, UI.theme.colors) | ||||
| UI:setDefaultDevice(UI.Device()) | ||||
|  | ||||
| for k,v in pairs(UI.colors) do | ||||
| 	Canvas.colorPalette[k] = Canvas.colorPalette[v] | ||||
| 	Canvas.grayscalePalette[k] = Canvas.grayscalePalette[v] | ||||
| end | ||||
|  | ||||
| return UI | ||||
|   | ||||
							
								
								
									
										174
									
								
								sys/modules/opus/ui/blit.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								sys/modules/opus/ui/blit.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| local colors = _G.colors | ||||
| local _rep   = string.rep | ||||
| local _sub   = string.sub | ||||
|  | ||||
| local Blit = { } | ||||
|  | ||||
| Blit.colorPalette = { } | ||||
| Blit.grayscalePalette = { } | ||||
|  | ||||
| for n = 1, 16 do | ||||
| 	Blit.colorPalette[2 ^ (n - 1)]     = _sub("0123456789abcdef", n, n) | ||||
| 	Blit.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) | ||||
| end | ||||
|  | ||||
| -- default palette | ||||
| Blit.palette = Blit.colorPalette | ||||
|  | ||||
| function Blit:init(t, args) | ||||
| 	if args then | ||||
| 		for k,v in pairs(args) do | ||||
| 			self[k] = v | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	if type(t) == 'string' then | ||||
| 		-- create a blit from a string | ||||
| 		self.text, self.bg, self.fg = Blit.toblit(t, args or { }) | ||||
|  | ||||
| 	elseif type(t) == 'number' then | ||||
| 		-- create a fixed width blit | ||||
| 		self.width = t | ||||
| 		self.text = _rep(' ', self.width) | ||||
| 		self.bg = _rep(self.palette[args.bg], self.width) | ||||
| 		self.fg = _rep(self.palette[args.fg], self.width) | ||||
|  | ||||
| 	else | ||||
| 		self.text = t.text | ||||
| 		self.bg = t.bg | ||||
| 		self.fg = t.fg | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Blit:write(x, text, bg, fg) | ||||
| 	self:insert(x, text, | ||||
| 		bg and _rep(self.palette[bg], #text), | ||||
| 		fg and _rep(self.palette[fg], #text)) | ||||
| end | ||||
|  | ||||
| function Blit:insert(x, text, bg, fg) | ||||
| 	if 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 | ||||
|  | ||||
| 			self.text = replace(self.text, text) | ||||
| 			if fg then | ||||
| 				self.fg = replace(self.fg, fg) | ||||
| 			end | ||||
| 			if bg then | ||||
| 				self.bg = replace(self.bg, bg) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Blit:sub(s, e) | ||||
| 	return Blit({ | ||||
| 		text = self.text:sub(s, e), | ||||
| 		bg = self.bg:sub(s, e), | ||||
| 		fg = self.fg:sub(s, e), | ||||
| 	}) | ||||
| end | ||||
|  | ||||
| function Blit:wrap(max) | ||||
| 	local lines = { } | ||||
| 	local data = self | ||||
|  | ||||
|     repeat | ||||
| 		if #data.text <= max then | ||||
| 			table.insert(lines, data) | ||||
| 			break | ||||
| 		elseif data.text:sub(max+1, max+1) == ' ' then | ||||
| 			table.insert(lines, data:sub(1, max)) | ||||
| 			data = data:sub(max + 2) | ||||
| 		else | ||||
| 			local x = data.text:sub(1, max) | ||||
| 			local s = x:match('(.*) ') or x | ||||
| 			table.insert(lines, data:sub(1, #s)) | ||||
| 			data = data:sub(#s + 1) | ||||
| 		end | ||||
| 		local t = data.text:match('^%s*(.*)') | ||||
| 		local spaces = #data.text - #t | ||||
| 		if spaces > 0 then | ||||
| 			data = data:sub(spaces + 1) | ||||
| 		end | ||||
| 	until not data.text or #data.text == 0 | ||||
|  | ||||
|     return lines | ||||
| end | ||||
|  | ||||
| -- convert a string of text to blit format doing color conversion | ||||
| -- and processing ansi color sequences | ||||
| function Blit.toblit(str, cs) | ||||
| 	local text, fg, bg = '', '', '' | ||||
|  | ||||
| 	if not cs.cbg then | ||||
| 		-- reset colors | ||||
| 		cs.rbg = cs.bg or colors.black | ||||
| 		cs.rfg = cs.fg or colors.white | ||||
| 		-- current colors | ||||
| 		cs.cbg = cs.rbg | ||||
| 		cs.cfg = cs.rfg | ||||
|  | ||||
| 		cs.palette = cs.palette or Blit.palette | ||||
| 	end | ||||
|  | ||||
| 	str = str:gsub('(.-)\027%[([%d;]+)m', | ||||
| 		function(k, seq) | ||||
| 			text = text .. k | ||||
| 			bg = bg .. string.rep(cs.palette[cs.cbg], #k) | ||||
| 			fg = fg .. string.rep(cs.palette[cs.cfg], #k) | ||||
| 			for color in string.gmatch(seq, "%d+") do | ||||
| 				color = tonumber(color) | ||||
| 				if color == 0 then | ||||
| 					-- reset to default | ||||
| 					cs.cfg = cs.rfg | ||||
| 					cs.cbg = cs.rbg | ||||
| 				elseif color > 20 then | ||||
| 					cs.cbg = 2 ^ (color - 21) | ||||
| 				else | ||||
| 					cs.cfg = 2 ^ (color - 1) | ||||
| 				end | ||||
| 			end | ||||
| 			return k | ||||
| 		end) | ||||
|  | ||||
| 	local k = str:sub(#text + 1) | ||||
| 	return text .. k, | ||||
| 		bg .. string.rep(cs.palette[cs.cbg], #k), | ||||
| 		fg .. string.rep(cs.palette[cs.cfg], #k) | ||||
| end | ||||
|  | ||||
| return setmetatable(Blit, { | ||||
| 	__call = function(_, ...) | ||||
| 		local obj = setmetatable({ }, { __index = Blit }) | ||||
| 		obj:init(...) | ||||
| 		return obj | ||||
| 	end | ||||
| }) | ||||
| @@ -9,28 +9,34 @@ local colors = _G.colors | ||||
|  | ||||
| local Canvas = class() | ||||
|  | ||||
| Canvas.__visualize = false | ||||
| 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) | ||||
| 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.x = 1 | ||||
| 	self.y = 1 | ||||
| 	self.layers = { } | ||||
| 	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 | ||||
|  | ||||
| @@ -46,16 +52,31 @@ function Canvas:init(args) | ||||
| 	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) | ||||
| 	for i = #self.lines, h do | ||||
| 	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 | ||||
| @@ -66,26 +87,24 @@ function Canvas:resize(w, h) | ||||
|  | ||||
| 	if w < self.width then | ||||
| 		for i = 1, h do | ||||
| 			self.lines[i].text = _sub(self.lines[i].text, 1, w) | ||||
| 			self.lines[i].fg = _sub(self.lines[i].fg, 1, w) | ||||
| 			self.lines[i].bg = _sub(self.lines[i].bg, 1, w) | ||||
| 			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 or colors.white], d) | ||||
| 		local bg = _rep(self.palette[self.bg or colors.black], d) | ||||
| 		local fg = _rep(self.palette[self.fg], d) | ||||
| 		local bg = _rep(self.palette[self.bg], d) | ||||
| 		for i = 1, h do | ||||
| 			self.lines[i].text = self.lines[i].text .. text | ||||
| 			self.lines[i].fg = self.lines[i].fg .. fg | ||||
| 			self.lines[i].bg = self.lines[i].bg .. bg | ||||
| 			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 | ||||
|  | ||||
| 	self.ex = self.x + w - 1 | ||||
| 	self.ey = self.y + h - 1 | ||||
| 	self.width = w | ||||
| 	self.height = h | ||||
| end | ||||
|  | ||||
| function Canvas:copy() | ||||
| @@ -105,30 +124,26 @@ function Canvas:copy() | ||||
| 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 | ||||
| 	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.layers) do | ||||
| 	for k, layer in pairs(self.parent.children) do | ||||
| 		if layer == self then | ||||
| 			self:setVisible(false) | ||||
| 			table.remove(self.parent.layers, k) | ||||
| 			table.remove(self.parent.children, k) | ||||
| 			break | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Canvas:setVisible(visible) | ||||
| 	self.visible = 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 | ||||
| @@ -137,11 +152,10 @@ end | ||||
|  | ||||
| -- Push a layer to the top | ||||
| function Canvas:raise() | ||||
| 	if self.parent then | ||||
| 		local layers = self.parent.layers or { } | ||||
| 		for k, v in pairs(layers) do | ||||
| 	if self.parent and self.parent.children then | ||||
| 		for k, v in pairs(self.parent.children) do | ||||
| 			if v == self then | ||||
| 				table.insert(layers, table.remove(layers, k)) | ||||
| 				table.insert(self.parent.children, table.remove(self.parent.children, k)) | ||||
| 				break | ||||
| 			end | ||||
| 		end | ||||
| @@ -161,54 +175,42 @@ 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 | ||||
|  | ||||
| 		-- fix ffs | ||||
| 		if x < 1 then | ||||
| 			text = _sub(text, 2 - x) | ||||
| 			if bg then | ||||
| 				bg = _sub(bg, 2 - x) | ||||
| 			end | ||||
| 			if fg then | ||||
| 				fg = _sub(fg, 2 - x) | ||||
| 			end | ||||
| 			tx = 2 - x | ||||
| 			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 fg then | ||||
| 				fg = _sub(fg, 1, self.width - x + 1) | ||||
| 			end | ||||
| 			width = #text | ||||
| 			tex = self.width - x + (tx or 1) | ||||
| 			width = tex - (tx or 1) + 1 | ||||
| 		end | ||||
|  | ||||
| 		if width > 0 then | ||||
|  | ||||
| 			local function replace(sstr, pos, rstr) | ||||
| 				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 | ||||
| 			local function replace(sstr, rstr) | ||||
| 				if tx or tex then | ||||
| 					rstr = _sub(rstr, tx or 1, tex) | ||||
| 				end | ||||
| 				return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) | ||||
| 				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] | ||||
| 			if line then | ||||
| 				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 | ||||
| 			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 | ||||
| @@ -224,15 +226,15 @@ function Canvas:writeLine(y, text, fg, bg) | ||||
| end | ||||
|  | ||||
| function Canvas:clearLine(y, bg, fg) | ||||
| 	fg = _rep(self.palette[fg or colors.white], self.width) | ||||
| 	bg = _rep(self.palette[bg or colors.black], self.width) | ||||
| 	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 colors.white], self.width) | ||||
| 	bg = _rep(self.palette[bg or colors.black], 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 | ||||
| @@ -246,13 +248,16 @@ function Canvas:isDirty() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Canvas:dirty() | ||||
| 	for i = 1, #self.lines do | ||||
| 		self.lines[i].dirty = true | ||||
| 	end | ||||
| 	if self.layers then | ||||
| 		for _, canvas in pairs(self.layers) do | ||||
| 			canvas:dirty() | ||||
| 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 | ||||
| @@ -278,115 +283,95 @@ function Canvas:applyPalette(palette) | ||||
| 	self.palette = palette | ||||
| end | ||||
|  | ||||
| function Canvas:render(device) | ||||
| 	local offset = { x = 0, y = 0 } | ||||
| 	local parent = self.parent | ||||
| 	while parent do | ||||
| 		offset.x = offset.x + parent.x - 1 | ||||
| 		offset.y = offset.y + parent.y - 1 | ||||
| 		parent = parent.parent | ||||
| 	end | ||||
| 	if #self.layers > 0 then | ||||
| 		self:__renderLayers(device, offset) | ||||
| 	else | ||||
| 		self:__blitRect(device, nil, { | ||||
| 			x = self.x + offset.x, | ||||
| 			y = self.y + offset.y | ||||
| 		}) | ||||
| 		self:clean() | ||||
| -- 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 coorespond to the output device. | ||||
| -- 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) | ||||
| 	if #self.layers > 0 then | ||||
| 		self.regions = self.regions or Region.new(self.x + offset.x, self.y + offset.y, self.ex + offset.x, self.ey + offset.y) | ||||
|  | ||||
| 		for i = 1, #self.layers do | ||||
| 			local canvas = self.layers[i] | ||||
| 			if canvas.visible then | ||||
|  | ||||
| 				-- punch out this area from the parent's canvas | ||||
| 				self:__punch(canvas, offset) | ||||
|  | ||||
| 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, | ||||
| 					canvas.y + offset.y, | ||||
| 					canvas.ex + offset.x, | ||||
| 					canvas.ey + offset.y) | ||||
| 					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)) | ||||
|  | ||||
| 				-- punch out any layers that overlap this one | ||||
| 				for j  = i + 1, #self.layers do | ||||
| 					if self.layers[j].visible then | ||||
| 						canvas:__punch(self.layers[j], offset) | ||||
| 					end | ||||
| 				end | ||||
| 				if #canvas.regions.region > 0 then | ||||
| 					canvas:__renderLayers(device, { | ||||
| 						x = canvas.x + offset.x - 1, | ||||
| 						y = canvas.y + offset.y - 1, | ||||
| 					}) | ||||
| 						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 | ||||
|  | ||||
| 		self:__blitClipped(device, offset) | ||||
| 		self.regions = nil | ||||
|  | ||||
| 	elseif self.regions and #self.regions.region > 0 then | ||||
| 		self:__blitClipped(device, offset) | ||||
| 		self.regions = nil | ||||
|  | ||||
| 	else | ||||
| 		self:__blitRect(device, nil, { | ||||
| 			x = self.x + offset.x, | ||||
| 			y = self.y + offset.y | ||||
| 		}) | ||||
| 		self.regions = nil | ||||
| 	end | ||||
| 	self:clean() | ||||
| end | ||||
|  | ||||
| function Canvas:__blitClipped(device, offset) | ||||
| 	if self.parent then | ||||
| 		-- contain the rendered region in the parent's region | ||||
| 		local p = Region.new(1, 1, | ||||
| 			self.parent.width + offset.x - self.x + 1, | ||||
| 			self.parent.height + offset.y - self.y + 1) | ||||
| 		self.regions:andRegion(p) | ||||
| 	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] }) | ||||
| 			  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:__punch(rect, offset) | ||||
| 	self.regions:subRect( | ||||
| 		rect.x + offset.x, | ||||
| 		rect.y + offset.y, | ||||
| 		rect.ex + offset.x, | ||||
| 		rect.ey + offset.y) | ||||
| end | ||||
|  | ||||
| -- performance can probably be improved by using one more buffer tied to the device | ||||
| function Canvas:__blitRect(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 | ||||
|  | ||||
| function Canvas:__blitRect(device, src, tgt, doubleBuffer) | ||||
| 	-- for visualizing updates on the screen | ||||
| 	if Canvas.__visualize then | ||||
| 	--[[ | ||||
| 	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) | ||||
| @@ -399,10 +384,11 @@ function Canvas:__blitRect(device, src, tgt) | ||||
| 			end | ||||
| 		end | ||||
| 		if drew then | ||||
| 			local t = os.clock() | ||||
| 			repeat until os.clock()-t > .2 | ||||
| 			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 | ||||
| @@ -412,8 +398,13 @@ function Canvas:__blitRect(device, src, tgt) | ||||
| 				fg = _sub(fg, src.x, src.ex) | ||||
| 				bg = _sub(bg, src.x, src.ex) | ||||
| 			end | ||||
| 			device.setCursorPos(tgt.x, tgt.y + i) | ||||
| 			device.blit(t, fg, bg) | ||||
| 			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 | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.ActiveLayer = class(UI.Window) | ||||
| UI.ActiveLayer.defaults = { | ||||
| 	UIElement = 'ActiveLayer', | ||||
| } | ||||
| function UI.ActiveLayer:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.ActiveLayer:enable(...) | ||||
| 	self.canvas:raise() | ||||
| 	self.canvas:setVisible(true) | ||||
| 	UI.Window.enable(self, ...) | ||||
| 	if self.parent.transitionHint then | ||||
| 		self:addTransition(self.parent.transitionHint) | ||||
| 	end | ||||
| 	self:focusFirst() | ||||
| end | ||||
|  | ||||
| function UI.ActiveLayer:disable() | ||||
| 	if self.canvas then | ||||
| 		self.canvas:setVisible(false) | ||||
| 	end | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
| @@ -2,32 +2,30 @@ local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Button = class(UI.Window) | ||||
| UI.Button.defaults = { | ||||
| 	UIElement = 'Button', | ||||
| 	text = 'button', | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	backgroundFocusColor = colors.gray, | ||||
| 	textFocusColor = colors.white, | ||||
| 	textInactiveColor = colors.gray, | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = 'lightGray', | ||||
| 	backgroundFocusColor = 'gray', | ||||
| 	textFocusColor = 'white', | ||||
| 	textInactiveColor = 'gray', | ||||
| 	textColor = 'black', | ||||
| 	centered = true, | ||||
| 	height = 1, | ||||
| 	focusIndicator = ' ', | ||||
| 	event = 'button_press', | ||||
| 	accelerators = { | ||||
| 		space = 'button_activate', | ||||
| 		[ ' ' ] = 'button_activate', | ||||
| 		enter = 'button_activate', | ||||
| 		mouse_click = 'button_activate', | ||||
| 	} | ||||
| } | ||||
| function UI.Button:setParent() | ||||
| function UI.Button:layout() | ||||
| 	if not self.width and not self.ex then | ||||
| 		self.width = #self.text + 2 | ||||
| 		self.width = self.noPadding and #self.text or #self.text + 2 | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Button:draw() | ||||
| @@ -35,13 +33,13 @@ function UI.Button:draw() | ||||
| 	local bg = self.backgroundColor | ||||
| 	local ind = ' ' | ||||
| 	if self.focused then | ||||
| 		bg = self.backgroundFocusColor | ||||
| 		fg = self.textFocusColor | ||||
| 		bg = self:getProperty('backgroundFocusColor') | ||||
| 		fg = self:getProperty('textFocusColor') | ||||
| 		ind = self.focusIndicator | ||||
| 	elseif self.inactive then | ||||
| 		fg = self.textInactiveColor | ||||
| 		fg = self:getProperty('textInactiveColor') | ||||
| 	end | ||||
| 	local text = ind .. self.text .. ' ' | ||||
| 	local text = self.noPadding and self.text or ind .. self.text .. ' ' | ||||
| 	if self.centered then | ||||
| 		self:clear(bg) | ||||
| 		self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) | ||||
| @@ -59,7 +57,7 @@ end | ||||
|  | ||||
| function UI.Button:eventHandler(event) | ||||
| 	if event.type == 'button_activate' then | ||||
| 		self:emit({ type = self.event, button = self }) | ||||
| 		self:emit({ type = self.event, button = self, element = self }) | ||||
| 		return true | ||||
| 	end | ||||
| 	return false | ||||
| @@ -73,7 +71,7 @@ function UI.Button.example() | ||||
| 		}, | ||||
| 		button2 = UI.Button { | ||||
| 			x = 2, y = 4, | ||||
| 			backgroundColor = colors.green, | ||||
| 			backgroundColor = 'green', | ||||
| 			event = 'custom_event', | ||||
| 		}, | ||||
| 		button3 = UI.Button { | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Checkbox = class(UI.Window) | ||||
| UI.Checkbox.defaults = { | ||||
| 	UIElement = 'Checkbox', | ||||
| @@ -11,9 +9,9 @@ UI.Checkbox.defaults = { | ||||
| 	leftMarker = UI.extChars and '\124' or '[', | ||||
| 	rightMarker = UI.extChars and '\124' or ']', | ||||
| 	value = false, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = colors.black, | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	textColor = 'white', | ||||
| 	backgroundColor = 'black', | ||||
| 	backgroundFocusColor = 'lightGray', | ||||
| 	height = 1, | ||||
| 	width = 3, | ||||
| 	accelerators = { | ||||
| @@ -21,11 +19,9 @@ UI.Checkbox.defaults = { | ||||
| 		mouse_click = 'checkbox_toggle', | ||||
| 	} | ||||
| } | ||||
| UI.Checkbox.inherits = { | ||||
| 	labelBackgroundColor = 'backgroundColor', | ||||
| } | ||||
| function UI.Checkbox:postInit() | ||||
| function UI.Checkbox:layout() | ||||
| 	self.width = self.label and #self.label + 4 or 3 | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Checkbox:draw() | ||||
|   | ||||
| @@ -11,8 +11,8 @@ UI.Chooser.defaults = { | ||||
| 	nochoice = 'Select', | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	textInactiveColor = colors.gray, | ||||
| 	leftIndicator = UI.extChars and '\17' or '<', | ||||
| 	rightIndicator = UI.extChars and '\16' or '>', | ||||
| 	leftIndicator = UI.extChars and '\171' or '<', | ||||
| 	rightIndicator = UI.extChars and '\187' or '>', | ||||
| 	height = 1, | ||||
| 	accelerators = { | ||||
| 		space = 'choice_next', | ||||
| @@ -20,7 +20,7 @@ UI.Chooser.defaults = { | ||||
| 		left  = 'choice_prev', | ||||
| 	} | ||||
| } | ||||
| function UI.Chooser:setParent() | ||||
| function UI.Chooser:layout() | ||||
| 	if not self.width and not self.ex then | ||||
| 		self.width = 1 | ||||
| 		for _,v in pairs(self.choices) do | ||||
| @@ -30,7 +30,7 @@ function UI.Chooser:setParent() | ||||
| 		end | ||||
| 		self.width = self.width + 4 | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Chooser:draw() | ||||
|   | ||||
| @@ -1,15 +1,11 @@ | ||||
| local Canvas = require('opus.ui.canvas') | ||||
| local class  = require('opus.class') | ||||
| local UI     = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Dialog = class(UI.SlideOut) | ||||
| UI.Dialog.defaults = { | ||||
| 	UIElement = 'Dialog', | ||||
| 	height = 7, | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = colors.white, | ||||
| 	noFill = true, | ||||
| 	okEvent ='dialog_ok', | ||||
| 	cancelEvent = 'dialog_cancel', | ||||
| } | ||||
| @@ -18,22 +14,36 @@ function UI.Dialog:postInit() | ||||
| 	self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title }) | ||||
| end | ||||
|  | ||||
| function UI.Dialog:show(...) | ||||
| 	local canvas = self.parent:getCanvas() | ||||
| 	self.oldPalette = canvas.palette | ||||
| 	canvas:applyPalette(Canvas.darkPalette) | ||||
| 	UI.SlideOut.show(self, ...) | ||||
| end | ||||
|  | ||||
| function UI.Dialog:hide(...) | ||||
| 	self.parent:getCanvas().palette = self.oldPalette | ||||
| 	UI.SlideOut.hide(self, ...) | ||||
| 	self.parent:draw() | ||||
| end | ||||
|  | ||||
| function UI.Dialog:eventHandler(event) | ||||
| 	if event.type == 'dialog_cancel' then | ||||
| 		self:hide() | ||||
| 	end | ||||
| 	return UI.SlideOut.eventHandler(self, event) | ||||
| end | ||||
|  | ||||
| function UI.Dialog.example() | ||||
| 	return UI.Dialog { | ||||
| 		title = 'Enter Starting Level', | ||||
| 		height = 7, | ||||
| 		form = UI.Form { | ||||
| 			y = 3, x = 2, height = 4, | ||||
| 			event = 'setStartLevel', | ||||
| 			cancelEvent = 'slide_hide', | ||||
| 			text = UI.Text { | ||||
| 				x = 5, y = 1, width = 20, | ||||
| 				textColor = 'gray', | ||||
| 			}, | ||||
| 			textEntry = UI.TextEntry { | ||||
| 				formKey = 'level', | ||||
| 				x = 15, y = 1, width = 7, | ||||
| 			}, | ||||
| 		}, | ||||
| 		statusBar = UI.StatusBar(), | ||||
| 		enable = function(self) | ||||
| 			require('opus.event').onTimeout(0, function() | ||||
| 				self:show() | ||||
| 				self:sync() | ||||
| 			end) | ||||
| 		end, | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -2,12 +2,10 @@ local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.DropMenu = class(UI.MenuBar) | ||||
| UI.DropMenu.defaults = { | ||||
| 	UIElement = 'DropMenu', | ||||
| 	backgroundColor = colors.white, | ||||
| 	backgroundColor = 'white', | ||||
| 	buttonClass = 'DropMenuItem', | ||||
| } | ||||
| function UI.DropMenu:layout() | ||||
| @@ -32,42 +30,53 @@ function UI.DropMenu:layout() | ||||
| 	self.height = #self.children + 1 | ||||
| 	self.width = maxWidth + 2 | ||||
|  | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| 	if self.x + self.width > self.parent.width then | ||||
| 		self.x = self.parent.width - self.width + 1 | ||||
| 	end | ||||
|  | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| end | ||||
|  | ||||
| function UI.DropMenu:enable() | ||||
| end | ||||
| 	local menuBar = self.parent:find(self.menuUid) | ||||
| 	local hasActive | ||||
|  | ||||
| function UI.DropMenu:show(x, y) | ||||
| 	self.x, self.y = x, y | ||||
| 	self.canvas:move(x, y) | ||||
| 	self.canvas:setVisible(true) | ||||
| 	for _,c in pairs(self.children) do | ||||
| 		if not c.spacer and menuBar then | ||||
| 			c.inactive = not menuBar:getActive(c) | ||||
| 		end | ||||
| 		if not c.inactive then | ||||
| 			hasActive = true | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	-- jump through a lot of hoops if all selections are inactive | ||||
| 	-- there's gotta be a better way | ||||
| 	-- lots of exception code just to handle drop menus | ||||
| 	self.focus = not hasActive and function() end | ||||
|  | ||||
| 	UI.Window.enable(self) | ||||
|  | ||||
| 	if self.focus then | ||||
| 		self:setFocus(self) | ||||
| 	else | ||||
| 		self:focusFirst() | ||||
| 	end | ||||
| 	self:draw() | ||||
| 	self:capture(self) | ||||
| 	self:focusFirst() | ||||
| end | ||||
|  | ||||
| function UI.DropMenu:hide() | ||||
| 	self:disable() | ||||
| 	self.canvas:setVisible(false) | ||||
| 	self:release(self) | ||||
| function UI.DropMenu:disable() | ||||
| 	UI.Window.disable(self) | ||||
| 	self:remove() | ||||
| end | ||||
|  | ||||
| function UI.DropMenu:eventHandler(event) | ||||
| 	if event.type == 'focus_lost' and self.enabled then | ||||
| 		if not Util.contains(self.children, event.focused) then | ||||
| 			self:hide() | ||||
| 		if not (Util.contains(self.children, event.focused) or event.focused == self) then | ||||
| 			self:disable() | ||||
| 		end | ||||
| 	elseif event.type == 'mouse_out' and self.enabled then | ||||
| 		self:hide() | ||||
| 		self:refocus() | ||||
| 		self:disable() | ||||
| 		self:setFocus(self.parent:find(self.lastFocus)) | ||||
| 	else | ||||
| 		return UI.MenuBar.eventHandler(self, event) | ||||
| 	end | ||||
| @@ -83,6 +92,15 @@ function UI.DropMenu.example() | ||||
| 					{ spacer = true }, | ||||
| 					{ text = 'Quit        ^q', event = 'quit'   }, | ||||
| 			} }, | ||||
| 			{ text = 'Edit', dropdown = { | ||||
| 				{ text = 'Copy',           event = 'run' }, | ||||
| 				{ text = 'Paste        s', event = 'shell'  }, | ||||
| 			} }, | ||||
| 			{ text = '\187', | ||||
| 				x = -3, | ||||
| 				dropdown = { | ||||
| 					{ text = 'Associations', event = 'associate' }, | ||||
| 			} }, | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,20 +1,18 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.DropMenuItem = class(UI.Button) | ||||
| UI.DropMenuItem.defaults = { | ||||
| 	UIElement = 'DropMenuItem', | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = colors.white, | ||||
| 	textFocusColor = colors.white, | ||||
| 	textInactiveColor = colors.lightGray, | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	textColor = 'black', | ||||
| 	backgroundColor = 'white', | ||||
| 	textFocusColor = 'white', | ||||
| 	textInactiveColor = 'lightGray', | ||||
| 	backgroundFocusColor = 'lightGray', | ||||
| } | ||||
| function UI.DropMenuItem:eventHandler(event) | ||||
| 	if event.type == 'button_activate' then | ||||
| 		self.parent:hide() | ||||
| 		self.parent:disable() | ||||
| 	end | ||||
| 	return UI.Button.eventHandler(self, event) | ||||
| end | ||||
|   | ||||
| @@ -1,62 +1,63 @@ | ||||
| local class    = require('opus.class') | ||||
| local Event    = require('opus.event') | ||||
| local Terminal = require('opus.terminal') | ||||
| local UI       = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Embedded = class(UI.Window) | ||||
| UI.Embedded.defaults = { | ||||
| 	UIElement = 'Embedded', | ||||
| 	backgroundColor = colors.black, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = 'black', | ||||
| 	textColor = 'white', | ||||
| 	maxScroll = 100, | ||||
| 	accelerators = { | ||||
| 		up = 'scroll_up', | ||||
| 		down = 'scroll_down', | ||||
| 	} | ||||
| } | ||||
| function UI.Embedded:setParent() | ||||
| 	UI.Window.setParent(self) | ||||
|  | ||||
| 	self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false) | ||||
| 	self.win.setMaxScroll(self.maxScroll) | ||||
|  | ||||
| 	local canvas = self:getCanvas() | ||||
| 	self.win.getCanvas().parent = canvas | ||||
| 	table.insert(canvas.layers, self.win.getCanvas()) | ||||
| 	self.canvas = self.win.getCanvas() | ||||
|  | ||||
| 	self.win.setCursorPos(1, 1) | ||||
| 	self.win.setBackgroundColor(self.backgroundColor) | ||||
| 	self.win.setTextColor(self.textColor) | ||||
| 	self.win.clear() | ||||
| end | ||||
|  | ||||
| function UI.Embedded:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if self.win then | ||||
| 		self.win.reposition(self.x, self.y, self.width, self.height) | ||||
|  | ||||
| 	if not self.win then | ||||
| 		local t | ||||
| 		function self.render() | ||||
| 			if not t then | ||||
| 				t = Event.onTimeout(0, function() | ||||
| 					t = nil | ||||
| 					if self.focused then | ||||
| 						self:setCursorPos(self.win.getCursorPos()) | ||||
| 					end | ||||
| 					self:sync() | ||||
| 				end) | ||||
| 			end | ||||
| 		end | ||||
| 		self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false) | ||||
| 		self.win.canvas = self | ||||
| 		self.win.setMaxScroll(self.maxScroll) | ||||
| 		self.win.setCursorPos(1, 1) | ||||
| 		self.win.setBackgroundColor(self.backgroundColor) | ||||
| 		self.win.setTextColor(self.textColor) | ||||
| 		self.win.clear() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Embedded:draw() | ||||
| 	self.canvas:dirty() | ||||
| 	self:dirty() | ||||
| end | ||||
|  | ||||
| function UI.Embedded:focus() | ||||
| 	-- allow scrolling | ||||
| 	if self.focused then | ||||
| 		self:setCursorBlink(self.win.getCursorBlink()) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Embedded:enable() | ||||
| 	self.canvas:setVisible(true) | ||||
| 	self.canvas:raise() | ||||
| 	if self.visible then | ||||
| 		-- the window will automatically update on changes | ||||
| 		-- the canvas does not need to be rendereed | ||||
| 		self.win.setVisible(true) | ||||
| 	end | ||||
| 	UI.Window.enable(self) | ||||
| 	self.canvas:dirty() | ||||
| 	self.win.setVisible(true) | ||||
| 	self:dirty() | ||||
| end | ||||
|  | ||||
| function UI.Embedded:disable() | ||||
| 	self.canvas:setVisible(false) | ||||
| 	self.win.setVisible(false) | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
| @@ -71,17 +72,12 @@ function UI.Embedded:eventHandler(event) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Embedded:focus() | ||||
| 	-- allow scrolling | ||||
| end | ||||
|  | ||||
| function UI.Embedded.example() | ||||
| 	local Event = require('opus.event') | ||||
| 	local Util  = require('opus.util') | ||||
| 	local term  = _G.term | ||||
|  | ||||
| 	return UI.Embedded { | ||||
| 		visible = true, | ||||
| 		y = 2, x = 2, ex = -2, ey = -2, | ||||
| 		enable = function (self) | ||||
| 			UI.Embedded.enable(self) | ||||
| 			Event.addRoutine(function() | ||||
| @@ -90,10 +86,11 @@ function UI.Embedded.example() | ||||
| 				term.redirect(oterm) | ||||
| 			end) | ||||
| 		end, | ||||
| 		eventHandler = function(_, event) | ||||
| 		eventHandler = function(self, event) | ||||
| 			if event.type == 'key' then | ||||
| 				return true | ||||
| 			end | ||||
| 			return UI.Embedded.eventHandler(self, event) | ||||
| 		end | ||||
| 	} | ||||
| end | ||||
|   | ||||
							
								
								
									
										118
									
								
								sys/modules/opus/ui/components/FileSelect.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								sys/modules/opus/ui/components/FileSelect.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| local class = require('opus.class') | ||||
| local UI   = require('opus.ui') | ||||
| local Util = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local fs     = _G.fs | ||||
|  | ||||
| UI.FileSelect = class(UI.Window) | ||||
| UI.FileSelect.defaults = { | ||||
| 	UIElement = 'FileSelect', | ||||
| } | ||||
| function UI.FileSelect:postInit() | ||||
|     self.grid = UI.ScrollingGrid { | ||||
|         x = 2, y = 2, ex = -2, ey = -4, | ||||
|         dir = '/', | ||||
|         sortColumn = 'name', | ||||
|         columns = { | ||||
|             { heading = 'Name', key = 'name' }, | ||||
|             { heading = 'Size', key = 'size', width = 5 } | ||||
|         }, | ||||
|         getDisplayValues = function(_, row) | ||||
|             if row.size then | ||||
|                 row = Util.shallowCopy(row) | ||||
|                 row.size = Util.toBytes(row.size) | ||||
|             end | ||||
|             return row | ||||
|         end, | ||||
|         getRowTextColor = function(_, file) | ||||
|             if file.isDir then | ||||
|                 return colors.cyan | ||||
|             end | ||||
|             if file.isReadOnly then | ||||
|                 return colors.pink | ||||
|             end | ||||
|             return colors.white | ||||
|         end, | ||||
|         sortCompare = function(self, a, b) | ||||
|             if self.sortColumn == 'size' then | ||||
|                 return a.size < b.size | ||||
|             end | ||||
|             if a.isDir == b.isDir then | ||||
|                 return a.name:lower() < b.name:lower() | ||||
|             end | ||||
|             return a.isDir | ||||
|         end, | ||||
|         draw = function(self) | ||||
|             local files = fs.listEx(self.dir) | ||||
|             if #self.dir > 0 then | ||||
|                 table.insert(files, { | ||||
|                     name = '..', | ||||
|                     isDir = true, | ||||
|                 }) | ||||
|             end | ||||
|             self:setValues(files) | ||||
|             self:setIndex(1) | ||||
|             UI.Grid.draw(self) | ||||
|         end, | ||||
|     } | ||||
|     self.path = UI.TextEntry { | ||||
|         x  =  2, | ||||
|         y  = -2, | ||||
|         ex = -11, | ||||
|         limit = 256, | ||||
|         accelerators = { | ||||
|             enter = 'path_enter', | ||||
|         } | ||||
|     } | ||||
|     self.cancel = UI.Button { | ||||
|         text = 'Cancel', | ||||
|         x = -9, | ||||
|         y = -2, | ||||
|         event = 'select_cancel', | ||||
|     } | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:draw() | ||||
|     self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray) | ||||
|     self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:enable(path) | ||||
|     self:setPath(path or '') | ||||
|     UI.Window.enable(self) | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:setPath(path) | ||||
| 	self.grid.dir = path | ||||
| 	while not fs.isDir(self.grid.dir) do | ||||
| 		self.grid.dir = fs.getDir(self.grid.dir) | ||||
| 	end | ||||
| 	self.path.value = self.grid.dir | ||||
| end | ||||
|  | ||||
| function UI.FileSelect:eventHandler(event) | ||||
| 	if event.type == 'grid_select' then | ||||
| 		self.grid.dir = fs.combine(self.grid.dir, event.selected.name) | ||||
| 		self.path.value = self.grid.dir | ||||
| 		if event.selected.isDir then | ||||
| 			self.grid:draw() | ||||
| 			self.path:draw() | ||||
|         else | ||||
|             self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self }) | ||||
|         end | ||||
|         return true | ||||
|  | ||||
|     elseif event.type == 'path_enter' then | ||||
|         if self.path.value then | ||||
|             if fs.isDir(self.path.value) then | ||||
|                 self:setPath(self.path.value) | ||||
|                 self.grid:draw() | ||||
|                 self.path:draw() | ||||
|             else | ||||
|                 self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self }) | ||||
|             end | ||||
|         end | ||||
|         return true | ||||
| 	end | ||||
| end | ||||
							
								
								
									
										16
									
								
								sys/modules/opus/ui/components/FlatButton.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								sys/modules/opus/ui/components/FlatButton.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.FlatButton = class(UI.Button) | ||||
| UI.FlatButton.defaults = { | ||||
| 	UIElement = 'FlatButton', | ||||
| 	textColor = 'black', | ||||
| 	textFocusColor = 'white', | ||||
| 	noPadding = true, | ||||
| } | ||||
| function UI.FlatButton:setParent() | ||||
| 	self.backgroundColor = self.parent:getProperty('backgroundColor') | ||||
| 	self.backgroundFocusColor = self.backgroundColor | ||||
|  | ||||
| 	UI.Button.setParent(self) | ||||
| end | ||||
| @@ -2,8 +2,6 @@ local class = require('opus.class') | ||||
| local Sound = require('opus.sound') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Form = class(UI.Window) | ||||
| UI.Form.defaults = { | ||||
| 	UIElement = 'Form', | ||||
| @@ -68,7 +66,7 @@ function UI.Form:createForm() | ||||
| 				table.insert(self.children, UI.Text { | ||||
| 					x = self.margin, | ||||
| 					y = child.y, | ||||
| 					textColor = colors.black, | ||||
| 					textColor = 'black', | ||||
| 					width = #child.formLabel, | ||||
| 					value = child.formLabel, | ||||
| 				}) | ||||
|   | ||||
| @@ -2,10 +2,8 @@ local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local os     = _G.os | ||||
| local _rep   = string.rep | ||||
| local _sub   = string.sub | ||||
|  | ||||
| local function safeValue(v) | ||||
| 	local t = type(v) | ||||
| @@ -23,18 +21,7 @@ function Writer:init(element, y) | ||||
| end | ||||
|  | ||||
| function Writer:write(s, width, align, bg, fg) | ||||
| 	local len = #tostring(s or '') | ||||
| 	if len > width then | ||||
| 		s = _sub(s, 1, width) | ||||
| 	end | ||||
| 	local padding = len < width and _rep(' ', width - len) | ||||
| 	if padding then | ||||
| 		if align == 'right' then | ||||
| 			s = padding .. s | ||||
| 		else | ||||
| 			s = s .. padding | ||||
| 		end | ||||
| 	end | ||||
| 	s = Util.widthify(s, width, align) | ||||
| 	self.element:write(self.x, self.y, s, bg, fg) | ||||
| 	self.x = self.x + width | ||||
| end | ||||
| @@ -56,16 +43,16 @@ UI.Grid.defaults = { | ||||
| 	disableHeader = false, | ||||
| 	headerHeight = 1, | ||||
| 	marginRight = 0, | ||||
| 	textColor = colors.white, | ||||
| 	textSelectedColor = colors.white, | ||||
| 	backgroundColor = colors.black, | ||||
| 	backgroundSelectedColor = colors.gray, | ||||
| 	headerBackgroundColor = colors.cyan, | ||||
| 	headerTextColor = colors.white, | ||||
| 	headerSortColor = colors.yellow, | ||||
| 	unfocusedTextSelectedColor = colors.white, | ||||
| 	unfocusedBackgroundSelectedColor = colors.gray, | ||||
| 	focusIndicator = UI.extChars and '\183' or '>', | ||||
| 	textColor = 'white', | ||||
| 	textSelectedColor = 'white', | ||||
| 	backgroundColor = 'black', | ||||
| 	backgroundSelectedColor = 'gray', | ||||
| 	headerBackgroundColor = 'primary', | ||||
| 	headerTextColor = 'white', | ||||
| 	headerSortColor = 'yellow', | ||||
| 	unfocusedTextSelectedColor = 'white', | ||||
| 	unfocusedBackgroundSelectedColor = 'gray', | ||||
| 	focusIndicator = UI.extChars and '\26' or '>', | ||||
| 	sortIndicator = ' ', | ||||
| 	inverseSortIndicator = UI.extChars and '\24' or '^', | ||||
| 	values = { }, | ||||
| @@ -83,8 +70,8 @@ UI.Grid.defaults = { | ||||
| 		[ 'control-f' ] = 'scroll_pageDown', | ||||
| 	}, | ||||
| } | ||||
| function UI.Grid:setParent() | ||||
| 	UI.Window.setParent(self) | ||||
| function UI.Grid:layout() | ||||
| 	UI.Window.layout(self) | ||||
|  | ||||
| 	for _,c in pairs(self.columns) do | ||||
| 		c.cw = c.width | ||||
| @@ -522,7 +509,7 @@ function UI.Grid.example() | ||||
| 			values = values, | ||||
| 			columns = { | ||||
| 				{ heading = 'key', key = 'key', width = 6,  }, | ||||
| 				{ heading = 'value', key = 'value', textColor = colors.yellow }, | ||||
| 				{ heading = 'value', key = 'value', textColor = 'yellow' }, | ||||
| 			}, | ||||
| 		}, | ||||
| 		autospace = UI.Grid { | ||||
|   | ||||
| @@ -1,19 +1,28 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local lookup = '0123456789abcdef' | ||||
|  | ||||
| -- handle files produced by Paint | ||||
| UI.Image = class(UI.Window) | ||||
| UI.Image.defaults = { | ||||
| 	UIElement = 'Image', | ||||
| 	event = 'button_press', | ||||
| } | ||||
| function UI.Image:setParent() | ||||
| 	if self.image then | ||||
| function UI.Image:postInit() | ||||
| 	if self.filename then | ||||
| 		self.image = Util.readLines(self.filename) | ||||
| 	end | ||||
|  | ||||
| 	if self.image and not (self.height or self.ey) then | ||||
| 		self.height = #self.image | ||||
| 	end | ||||
| 	if self.image and not self.width then | ||||
| 		self.width = #self.image[1] | ||||
| 	if self.image and not (self.width or self.ex) then | ||||
| 		for i = 1, self.height do | ||||
| 			self.width = math.max(self.width or 0, #self.image[i]) | ||||
| 		end | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| end | ||||
|  | ||||
| function UI.Image:draw() | ||||
| @@ -22,19 +31,22 @@ function UI.Image:draw() | ||||
| 		for y = 1, #self.image do | ||||
| 			local line = self.image[y] | ||||
| 			for x = 1, #line do | ||||
| 				local ch = line[x] | ||||
| 				if type(ch) == 'number' then | ||||
| 					if ch > 0 then | ||||
| 						self:write(x, y, ' ', ch) | ||||
| 					end | ||||
| 				else | ||||
| 					self:write(x, y, ch) | ||||
| 				local ch = lookup:find(line:sub(x, x)) | ||||
| 				if ch then | ||||
| 					self:write(x, y, ' ', 2 ^ (ch -1)) | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.Image:setImage(image) | ||||
| 	self.image = image | ||||
| end | ||||
|  | ||||
| function UI.Image.example() | ||||
| 	return UI.Image { | ||||
| 		filename = 'test.paint', | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -13,8 +13,7 @@ function UI.Menu:postInit() | ||||
| 	self.pageSize = #self.menuItems | ||||
| end | ||||
|  | ||||
| function UI.Menu:setParent() | ||||
| 	UI.Grid.setParent(self) | ||||
| function UI.Menu:layout() | ||||
| 	self.itemWidth = 1 | ||||
| 	for _,v in pairs(self.values) do | ||||
| 		if #v.prompt > self.itemWidth then | ||||
| @@ -28,6 +27,7 @@ function UI.Menu:setParent() | ||||
| 	else | ||||
| 		self.width = self.itemWidth + 2 | ||||
| 	end | ||||
| 	UI.Grid.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Menu:center() | ||||
|   | ||||
| @@ -1,28 +1,15 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| local function getPosition(element) | ||||
| 	local x, y = 1, 1 | ||||
| 	repeat | ||||
| 		x = element.x + x - 1 | ||||
| 		y = element.y + y - 1 | ||||
| 		element = element.parent | ||||
| 	until not element | ||||
| 	return x, y | ||||
| end | ||||
|  | ||||
| UI.MenuBar = class(UI.Window) | ||||
| UI.MenuBar.defaults = { | ||||
| 	UIElement = 'MenuBar', | ||||
| 	buttons = { }, | ||||
| 	height = 1, | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = 'secondary', | ||||
| 	textColor = 'black', | ||||
| 	spacing = 2, | ||||
| 	lastx = 1, | ||||
| 	showBackButton = false, | ||||
| 	buttonClass = 'MenuItem', | ||||
| } | ||||
| function UI.MenuBar:postInit() | ||||
| @@ -62,10 +49,6 @@ function UI.MenuBar:addButtons(buttons) | ||||
| 			else | ||||
| 				table.insert(self.children, button) | ||||
| 			end | ||||
|  | ||||
| 			if button.dropdown then | ||||
| 				button.dropmenu = UI.DropMenu { buttons = button.dropdown } | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 	if self.parent then | ||||
| @@ -78,23 +61,28 @@ function UI.MenuBar:getActive(menuItem) | ||||
| end | ||||
|  | ||||
| function UI.MenuBar:eventHandler(event) | ||||
| 	if event.type == 'button_press' and event.button.dropmenu then | ||||
| 		if event.button.dropmenu.enabled then | ||||
| 			event.button.dropmenu:hide() | ||||
| 			self:refocus() | ||||
| 			return true | ||||
| 		else | ||||
| 			local x, y = getPosition(event.button) | ||||
| 			if x + event.button.dropmenu.width > self.width then | ||||
| 				x = self.width - event.button.dropmenu.width + 1 | ||||
| 			end | ||||
| 			for _,c in pairs(event.button.dropmenu.children) do | ||||
| 				if not c.spacer then | ||||
| 					c.inactive = not self:getActive(c) | ||||
| 				end | ||||
| 			end | ||||
| 			event.button.dropmenu:show(x, y + 1) | ||||
| 	if event.type == 'button_press' and event.button.dropdown then | ||||
| 		local function getPosition(element) | ||||
| 			local x, y = 1, 1 | ||||
| 			repeat | ||||
| 				x = element.x + x - 1 | ||||
| 				y = element.y + y - 1 | ||||
| 				element = element.parent | ||||
| 			until not element | ||||
| 			return x, y | ||||
| 		end | ||||
|  | ||||
| 		local x, y = getPosition(event.button) | ||||
|  | ||||
| 		local menu = UI.DropMenu { | ||||
| 			buttons = event.button.dropdown, | ||||
| 			x = x, | ||||
| 			y = y + 1, | ||||
| 			lastFocus = event.button.uid, | ||||
| 			menuUid = self.uid, | ||||
| 		} | ||||
| 		self.parent:add({ dropmenu = menu }) | ||||
|  | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
| @@ -103,7 +91,8 @@ function UI.MenuBar.example() | ||||
| 	return UI.MenuBar { | ||||
| 		buttons = { | ||||
| 			{ text = 'Choice1', event = 'event1' }, | ||||
| 			{ text = 'Choice2', event = 'event2' }, | ||||
| 			{ text = 'Choice2', event = 'event2', inactive = true }, | ||||
| 			{ text = 'Choice3', event = 'event3' }, | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,13 +1,9 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.MenuItem = class(UI.Button) | ||||
| UI.MenuItem = class(UI.FlatButton) | ||||
| UI.MenuItem.defaults = { | ||||
| 	UIElement = 'MenuItem', | ||||
| 	textColor = colors.black, | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	textFocusColor = colors.white, | ||||
| 	backgroundFocusColor = colors.lightGray, | ||||
| 	noPadding = false, | ||||
| 	textInactiveColor = 'gray', | ||||
| } | ||||
|   | ||||
							
								
								
									
										31
									
								
								sys/modules/opus/ui/components/MiniSlideOut.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								sys/modules/opus/ui/components/MiniSlideOut.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.MiniSlideOut = class(UI.SlideOut) | ||||
| UI.MiniSlideOut.defaults = { | ||||
| 	UIElement = 'MiniSlideOut', | ||||
|     noFill = true, | ||||
|     backgroundColor = 'primary', | ||||
|     height = 1, | ||||
| } | ||||
| function UI.MiniSlideOut:postInit() | ||||
|     self.close_button = UI.Button { | ||||
|         x = -1, | ||||
|         backgroundColor = self.backgroundColor, | ||||
|         backgroundFocusColor = self.backgroundColor, | ||||
|         text = 'x', | ||||
|         event = 'slide_hide', | ||||
|         noPadding = true, | ||||
|     } | ||||
|     if self.label then | ||||
|         self.label_text = UI.Text { | ||||
|             x = 2, | ||||
|             value = self.label, | ||||
|         } | ||||
|     end | ||||
| end | ||||
|  | ||||
| function UI.MiniSlideOut:show(...) | ||||
|     UI.SlideOut.show(self, ...) | ||||
|     self:addTransition('slideLeft', { easing = 'outBounce' }) | ||||
| end | ||||
| @@ -5,17 +5,18 @@ UI.NftImage = class(UI.Window) | ||||
| UI.NftImage.defaults = { | ||||
| 	UIElement = 'NftImage', | ||||
| } | ||||
| function UI.NftImage:setParent() | ||||
| 	if self.image then | ||||
| function UI.NftImage:postInit() | ||||
| 	if self.image and not (self.ey or self.height) then | ||||
| 		self.height = self.image.height | ||||
| 	end | ||||
| 	if self.image and not self.width then | ||||
| 	if self.image and not (self.ex or self.width) then | ||||
| 		self.width = self.image.width | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| end | ||||
|  | ||||
| function UI.NftImage:draw() | ||||
| 	self:clear() | ||||
|  | ||||
| 	if self.image then | ||||
| 		-- due to blittle, the background and foreground transparent | ||||
| 		-- color is the same as the background color | ||||
| @@ -25,8 +26,6 @@ function UI.NftImage:draw() | ||||
| 				self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg) | ||||
| 			end | ||||
| 		end | ||||
| 	else | ||||
| 		self:clear() | ||||
| 	end | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -4,36 +4,34 @@ local Sound = require('opus.sound') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Notification = class(UI.Window) | ||||
| UI.Notification.defaults = { | ||||
| 	UIElement = 'Notification', | ||||
| 	backgroundColor = colors.gray, | ||||
| 	backgroundColor = 'gray', | ||||
| 	closeInd = UI.extChars and '\215' or '*', | ||||
| 	height = 3, | ||||
| 	timeout = 3, | ||||
| 	anchor = 'bottom', | ||||
| } | ||||
| function UI.Notification:draw() | ||||
| function UI.Notification.draw() | ||||
| end | ||||
|  | ||||
| function UI.Notification:enable() | ||||
| function UI.Notification.enable() | ||||
| end | ||||
|  | ||||
| function UI.Notification:error(value, timeout) | ||||
| 	self.backgroundColor = colors.red | ||||
| 	self.backgroundColor = 'red' | ||||
| 	Sound.play('entity.villager.no', .5) | ||||
| 	self:display(value, timeout) | ||||
| end | ||||
|  | ||||
| function UI.Notification:info(value, timeout) | ||||
| 	self.backgroundColor = colors.lightGray | ||||
| 	self.backgroundColor = 'lightGray' | ||||
| 	self:display(value, timeout) | ||||
| end | ||||
|  | ||||
| function UI.Notification:success(value, timeout) | ||||
| 	self.backgroundColor = colors.green | ||||
| 	self.backgroundColor = 'green' | ||||
| 	self:display(value, timeout) | ||||
| end | ||||
|  | ||||
| @@ -43,32 +41,34 @@ function UI.Notification:cancel() | ||||
| 		self.timer = nil | ||||
| 	end | ||||
|  | ||||
| 	if self.canvas then | ||||
| 		self.enabled = false | ||||
| 		self.canvas:removeLayer() | ||||
| 		self.canvas = nil | ||||
| 	end | ||||
| 	self:disable() | ||||
| end | ||||
|  | ||||
| function UI.Notification:display(value, timeout) | ||||
| 	self:cancel() | ||||
| 	self.enabled = true | ||||
| 	local lines = Util.wordWrap(value, self.width - 3) | ||||
|  | ||||
| 	self.enabled = true | ||||
| 	self.height = #lines | ||||
|  | ||||
| 	if self.anchor == 'bottom' then | ||||
| 		self.y = self.parent.height - self.height + 1 | ||||
| 		self.canvas = self:addLayer(self.backgroundColor, self.textColor) | ||||
| 		self:addTransition('expandUp', { ticks = self.height }) | ||||
| 	else | ||||
| 		self.canvas = self:addLayer(self.backgroundColor, self.textColor) | ||||
| 		self.y = 1 | ||||
| 	end | ||||
| 	self.canvas:setVisible(true) | ||||
|  | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| 	self:raise() | ||||
| 	self:clear() | ||||
| 	for k,v in pairs(lines) do | ||||
| 		self:write(2, k, v) | ||||
| 	end | ||||
| 	self:write(self.width, 1, self.closeInd) | ||||
|  | ||||
| 	if self.timer then | ||||
| 		Event.off(self.timer) | ||||
| 		self.timer = nil | ||||
| 	end | ||||
|  | ||||
| 	timeout = timeout or self.timeout | ||||
| 	if timeout > 0 then | ||||
| @@ -77,7 +77,6 @@ function UI.Notification:display(value, timeout) | ||||
| 			self:sync() | ||||
| 		end) | ||||
| 	else | ||||
| 		self:write(self.width, 1, self.closeInd) | ||||
| 		self:sync() | ||||
| 	end | ||||
| end | ||||
| @@ -92,7 +91,7 @@ function UI.Notification:eventHandler(event) | ||||
| end | ||||
|  | ||||
| function UI.Notification.example() | ||||
| 	return UI.ActiveLayer { | ||||
| 	return UI.Window { | ||||
| 		notify1 = UI.Notification { | ||||
| 			anchor = 'top', | ||||
| 		}, | ||||
| @@ -111,7 +110,9 @@ function UI.Notification.example() | ||||
| 			if event.type == 'test_success' then | ||||
| 				self.notify1:success('Example text') | ||||
| 			elseif event.type == 'test_error' then | ||||
| 				self.notify2:error('Example text', 0) | ||||
| 				self.notify2:error([[Example text test test | ||||
| test test test test test | ||||
| test test test]], 0) | ||||
| 			end | ||||
| 		end, | ||||
| 	} | ||||
|   | ||||
| @@ -1,60 +1,31 @@ | ||||
| local Canvas = require('opus.ui.canvas') | ||||
| local class  = require('opus.class') | ||||
| local UI     = require('opus.ui') | ||||
| local Util   = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| -- need to add offsets to this test | ||||
| local function getPosition(element) | ||||
| 	local x, y = 1, 1 | ||||
| 	repeat | ||||
| 		x = element.x + x - 1 | ||||
| 		y = element.y + y - 1 | ||||
| 		element = element.parent | ||||
| 	until not element | ||||
| 	return x, y | ||||
| end | ||||
|  | ||||
| UI.Page = class(UI.Window) | ||||
| UI.Page.defaults = { | ||||
| 	UIElement = 'Page', | ||||
| 	accelerators = { | ||||
| 		down = 'focus_next', | ||||
| 		scroll_down = 'focus_next', | ||||
| 		enter = 'focus_next', | ||||
| 		tab = 'focus_next', | ||||
| 		['shift-tab' ] = 'focus_prev', | ||||
| 		up = 'focus_prev', | ||||
| 		scroll_up = 'focus_prev', | ||||
| 	}, | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = 'primary', | ||||
| 	textColor = 'white', | ||||
| } | ||||
| function UI.Page:postInit() | ||||
| 	self.parent = self.parent or UI.defaultDevice | ||||
| 	self.parent = self.parent or UI.term | ||||
| 	self.__target = self | ||||
| 	self.canvas = Canvas({ | ||||
| 		x = 1, y = 1, width = self.parent.width, height = self.parent.height, | ||||
| 		isColor = self.parent.isColor, | ||||
| 	}) | ||||
| 	self.canvas:clear(self.backgroundColor, self.textColor) | ||||
| end | ||||
|  | ||||
| function UI.Page:enable() | ||||
| 	self.canvas.visible = true | ||||
| 	UI.Window.enable(self) | ||||
|  | ||||
| 	if not self.focused or not self.focused.enabled then | ||||
| 		self:focusFirst() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Page:disable() | ||||
| 	self.canvas.visible = false | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
|  | ||||
| function UI.Page:sync() | ||||
| 	if self.enabled then | ||||
| 		self:checkFocus() | ||||
| 		self.parent:setCursorBlink(self.focused and self.focused.cursorBlink) | ||||
| 		self.parent:sync() | ||||
| 	end | ||||
| end | ||||
| @@ -73,22 +44,23 @@ function UI.Page:pointToChild(x, y) | ||||
| 	if self.__target == self then | ||||
| 		return UI.Window.pointToChild(self, x, y) | ||||
| 	end | ||||
| 	x = x + self.offx - self.x + 1 | ||||
| 	y = y + self.offy - self.y + 1 | ||||
| --[[ | ||||
| 	-- this is supposed to fix when there are multiple sub canvases | ||||
| 	local absX, absY = getPosition(self.__target) | ||||
| 	if self.__target.canvas then | ||||
| 		x = x - (self.__target.canvas.x - self.__target.x) | ||||
| 		y = y - (self.__target.canvas.y - self.__target.y) | ||||
| 		_syslog({'raw', self.__target.canvas.y, self.__target.y}) | ||||
|  | ||||
| 	local function getPosition(element) | ||||
| 		local x, y = 1, 1 | ||||
| 		repeat | ||||
| 			x = element.x + x - 1 | ||||
| 			y = element.y + y - 1 | ||||
| 			element = element.parent | ||||
| 		until not element | ||||
| 		return x, y | ||||
| 	end | ||||
| 	]] | ||||
| 	return self.__target:pointToChild(x, y) | ||||
|  | ||||
| 	local absX, absY = getPosition(self.__target) | ||||
| 	return self.__target:pointToChild(x - absX + self.__target.x, y - absY + self.__target.y) | ||||
| end | ||||
|  | ||||
| function UI.Page:getFocusables() | ||||
| 	if self.__target == self or self.__target.pageType ~= 'modal' then | ||||
| 	if self.__target == self or not self.__target.modal then | ||||
| 		return UI.Window.getFocusables(self) | ||||
| 	end | ||||
| 	return self.__target:getFocusables() | ||||
| @@ -149,12 +121,17 @@ function UI.Page:setFocus(child) | ||||
| 	if not child.focused then | ||||
| 		child.focused = true | ||||
| 		child:emit({ type = 'focus_change', focused = child }) | ||||
| 		--self:emit({ type = 'focus_change', focused = child }) | ||||
| 	end | ||||
|  | ||||
| 	child:focus() | ||||
| end | ||||
|  | ||||
| function UI.Page:checkFocus() | ||||
| 	if not self.focused or not self.focused.enabled then | ||||
| 		self.__target:focusFirst() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Page:eventHandler(event) | ||||
| 	if self.focused then | ||||
| 		if event.type == 'focus_next' then | ||||
|   | ||||
| @@ -1,39 +1,31 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.ProgressBar = class(UI.Window) | ||||
| UI.ProgressBar.defaults = { | ||||
| 	UIElement = 'ProgressBar', | ||||
| 	backgroundColor = colors.gray, | ||||
| 	backgroundColor = 'gray', | ||||
| 	height = 1, | ||||
| 	progressColor = colors.lime, | ||||
| 	progressColor = 'lime', | ||||
| 	progressChar = UI.extChars and '\153' or ' ', | ||||
| 	fillChar = ' ', | ||||
| 	fillColor = colors.gray, | ||||
| 	textColor = colors.green, | ||||
| 	fillColor = 'gray', | ||||
| 	textColor = 'green', | ||||
| 	value = 0, | ||||
| } | ||||
| function UI.ProgressBar:draw() | ||||
| 	local width = math.ceil(self.value / 100 * self.width) | ||||
|  | ||||
| 	local filler = string.rep(self.fillChar, self.width) | ||||
| 	local progress = string.rep(self.progressChar, width) | ||||
|  | ||||
| 	for i = 1, self.height do | ||||
| 		self:write(1, i, filler, nil, self.fillColor) | ||||
| 		self:write(1, i, progress, self.progressColor) | ||||
| 	end | ||||
| 	self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor) | ||||
| 	self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor) | ||||
| end | ||||
|  | ||||
| function UI.ProgressBar.example() | ||||
| 	local Event = require('opus.event') | ||||
| 	return UI.ProgressBar { | ||||
| 		x = 2, ex = -2, y = 2, | ||||
| 		x = 2, ex = -2, y = 2, height = 2, | ||||
| 		focus = function() end, | ||||
| 		enable = function(self) | ||||
| 			Event.onInterval(.25, function() | ||||
| 			require('opus.event').onInterval(.25, function() | ||||
| 				self.value = self.value == 100 and 0 or self.value + 5 | ||||
| 				self:draw() | ||||
| 				self:sync() | ||||
|   | ||||
							
								
								
									
										27
									
								
								sys/modules/opus/ui/components/Question.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								sys/modules/opus/ui/components/Question.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.Question = class(UI.MiniSlideOut) | ||||
| UI.Question.defaults = { | ||||
|     UIElement = 'Question', | ||||
|     accelerators = { | ||||
|         y = 'question_yes', | ||||
|         n = 'question_no', | ||||
|     } | ||||
| } | ||||
| function UI.Question:postInit() | ||||
|     local x = self.label and #self.label + 3 or 1 | ||||
|  | ||||
|     self.yes_button = UI.Button { | ||||
|         x = x, | ||||
|         text = 'Yes', | ||||
|         backgroundColor = 'primary', | ||||
|         event = 'question_yes', | ||||
|     } | ||||
|     self.no_button = UI.Button { | ||||
|         x = x + 5, | ||||
|         text = 'No', | ||||
|         backgroundColor = 'primary', | ||||
|         event = 'question_no', | ||||
|     } | ||||
| end | ||||
| @@ -17,7 +17,13 @@ UI.ScrollBar.defaults = { | ||||
| 	ey = -1, | ||||
| } | ||||
| function UI.ScrollBar:draw() | ||||
| 	local view = self.parent:getViewArea() | ||||
| 	local parent = self.target or self.parent --self:find(self.target) | ||||
| 	local view = parent:getViewArea() | ||||
|  | ||||
| 	self:clear() | ||||
|  | ||||
| 	-- ... | ||||
| 	self:write(1, 1, ' ', view.fill) | ||||
|  | ||||
| 	if view.totalHeight > view.height then | ||||
| 		local maxScroll = view.totalHeight - view.height | ||||
| @@ -27,7 +33,7 @@ function UI.ScrollBar:draw() | ||||
|  | ||||
| 		local row = view.y | ||||
| 		if not view.static then  -- does the container scroll ? | ||||
| 			self.height = view.totalHeight | ||||
| 			self:reposition(self.x, self.y, self.width, view.totalHeight) | ||||
| 		end | ||||
|  | ||||
| 		for i = 1, view.height - 2 do | ||||
| @@ -56,16 +62,17 @@ end | ||||
| function UI.ScrollBar:eventHandler(event) | ||||
| 	if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then | ||||
| 		if event.x == 1 then | ||||
| 			local view = self.parent:getViewArea() | ||||
| 			local parent = self.target or self.parent --self:find(self.target) | ||||
| 			local view = parent:getViewArea() | ||||
| 			if view.totalHeight > view.height then | ||||
| 				if event.y == view.y then | ||||
| 					self:emit({ type = 'scroll_up'}) | ||||
| 					parent:emit({ type = 'scroll_up'}) | ||||
| 				elseif event.y == view.y + view.height - 1 then | ||||
| 					self:emit({ type = 'scroll_down'}) | ||||
| 					parent:emit({ type = 'scroll_down'}) | ||||
| 				else | ||||
| 					local percent = (event.y - view.y) / (view.height - 2) | ||||
| 					local y = math.floor((view.totalHeight - view.height) * percent) | ||||
| 					self:emit({ type = 'scroll_to', offset = y }) | ||||
| 					parent :emit({ type = 'scroll_to', offset = y }) | ||||
| 				end | ||||
| 			end | ||||
| 			return true | ||||
|   | ||||
| @@ -29,6 +29,7 @@ function UI.ScrollingGrid:getViewArea() | ||||
| 		height      = self.pageSize,           -- viewable height | ||||
| 		totalHeight = Util.size(self.values),  -- total height | ||||
| 		offsetY     = self.scrollOffset,       -- scroll offset | ||||
| 		fill        = not self.disableHeader and self.headerBackgroundColor, | ||||
| 	} | ||||
| end | ||||
|  | ||||
| @@ -57,3 +58,21 @@ function UI.ScrollingGrid:setIndex(index) | ||||
| 	end | ||||
| 	UI.Grid.setIndex(self, index) | ||||
| end | ||||
|  | ||||
| function UI.ScrollingGrid.example() | ||||
| 	local values = { } | ||||
| 	for i = 1, 20 do | ||||
| 		table.insert(values, { key = 'key' .. i, value = 'value' .. i }) | ||||
| 	end | ||||
| 	return UI.ScrollingGrid { | ||||
| 		values = values, | ||||
| 		sortColumn = 'key', | ||||
| 		columns = { | ||||
| 			{ heading = 'key', key = 'key' }, | ||||
| 			{ heading = 'value', key = 'value' }, | ||||
| 		}, | ||||
| 		accelerators = { | ||||
| 			grid_select = 'custom_select', | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
| @@ -4,17 +4,9 @@ local UI    = require('opus.ui') | ||||
| UI.SlideOut = class(UI.Window) | ||||
| UI.SlideOut.defaults = { | ||||
| 	UIElement = 'SlideOut', | ||||
| 	pageType = 'modal', | ||||
| 	transitionHint = 'expandUp', | ||||
| 	modal = true, | ||||
| } | ||||
| function UI.SlideOut:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:enable() | ||||
| end | ||||
|  | ||||
| @@ -27,24 +19,20 @@ function UI.SlideOut:toggle() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:show(...) | ||||
| 	self:addTransition('expandUp') | ||||
| 	self.canvas:raise() | ||||
| 	self.canvas:setVisible(true) | ||||
| 	UI.Window.enable(self, ...) | ||||
| 	self:draw() | ||||
| 	self:capture(self) | ||||
| 	self:focusFirst() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:disable() | ||||
| 	self.canvas:setVisible(false) | ||||
| 	UI.Window.disable(self) | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:hide() | ||||
| 	self:disable() | ||||
| 	self:release(self) | ||||
| 	self:refocus() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:draw() | ||||
| 	if not self.noFill then | ||||
| 		self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), 'black', 'gray') | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.SlideOut:eventHandler(event) | ||||
| @@ -59,24 +47,27 @@ function UI.SlideOut:eventHandler(event) | ||||
| end | ||||
|  | ||||
| function UI.SlideOut.example() | ||||
| 	-- for the transistion to work properly, the parent must have a canvas | ||||
| 	return UI.ActiveLayer { | ||||
| 		y = 2, | ||||
| 	return UI.Window { | ||||
| 		y = 3, | ||||
| 		backgroundColor = 2048, | ||||
| 		button = UI.Button { | ||||
| 			x = 2, y = 5, | ||||
| 			text = 'show', | ||||
| 		}, | ||||
| 		slideOut = UI.SlideOut { | ||||
| 			backgroundColor = _G.colors.yellow, | ||||
| 			y = -4, height = 4, x = 3, ex = -3, | ||||
| 			backgroundColor = 16, | ||||
| 			y = -7, height = 4, x = 3, ex = -3, | ||||
| 			titleBar = UI.TitleBar { | ||||
| 				title = 'test', | ||||
| 			}, | ||||
| 			button = UI.Button { | ||||
| 				x = 2, y = 2, | ||||
| 				text = 'hide', | ||||
| 				--visualize = true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		eventHandler = function (self, event) | ||||
| 			if event.type == 'button_press' then | ||||
| 				self.slideOut.canvas.xxx = true | ||||
| 				self.slideOut:toggle() | ||||
| 			end | ||||
| 		end, | ||||
|   | ||||
| @@ -2,17 +2,15 @@ local class  = require('opus.class') | ||||
| local UI     = require('opus.ui') | ||||
| local Util   = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Slider = class(UI.Window) | ||||
| UI.Slider.defaults = { | ||||
| 	UIElement = 'Slider', | ||||
| 	height = 1, | ||||
| 	barChar = UI.extChars and '\140' or '-', | ||||
| 	barColor = colors.gray, | ||||
| 	barColor = 'gray', | ||||
| 	sliderChar = UI.extChars and '\143' or '\124', | ||||
| 	sliderColor = colors.blue, | ||||
| 	sliderFocusColor = colors.lightBlue, | ||||
| 	sliderColor = 'blue', | ||||
| 	sliderFocusColor = 'lightBlue', | ||||
| 	leftBorder = UI.extChars and '\141' or '\124', | ||||
| 	rightBorder = UI.extChars and '\142' or '\124', | ||||
| 	value = 0, | ||||
| @@ -57,8 +55,16 @@ end | ||||
|  | ||||
| function UI.Slider:eventHandler(event) | ||||
| 	if event.type == "mouse_down" or event.type == "mouse_drag" then | ||||
|  | ||||
| 		local pos = event.x - 1 | ||||
| 		if event.type == 'mouse_down' then | ||||
| 			self.anchor = event.x - 1 | ||||
| 		else | ||||
| 			pos = self.anchor + event.dx | ||||
| 		end | ||||
|  | ||||
| 		local range = self.max - self.min | ||||
| 		local i = (event.x - 1) / (self.width - 1) | ||||
| 		local i = pos / (self.width - 1) | ||||
| 		self.value = self.min + (i * range) | ||||
| 		self:emit({ type = self.event, value = self.value, element = self }) | ||||
| 		self:draw() | ||||
|   | ||||
| @@ -3,17 +3,16 @@ local Event = require('opus.event') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.StatusBar = class(UI.Window) | ||||
| UI.StatusBar.defaults = { | ||||
| 	UIElement = 'StatusBar', | ||||
| 	backgroundColor = colors.lightGray, | ||||
| 	textColor = colors.gray, | ||||
| 	backgroundColor = 'lightGray', | ||||
| 	textColor = 'gray', | ||||
| 	height = 1, | ||||
| 	ey = -1, | ||||
| } | ||||
| function UI.StatusBar:adjustWidth() | ||||
| function UI.StatusBar:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	-- Can only have 1 adjustable width | ||||
| 	if self.columns then | ||||
| 		local w = self.width - #self.columns - 1 | ||||
| @@ -31,16 +30,6 @@ function UI.StatusBar:adjustWidth() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.StatusBar:resize() | ||||
| 	UI.Window.resize(self) | ||||
| 	self:adjustWidth() | ||||
| end | ||||
|  | ||||
| function UI.StatusBar:setParent() | ||||
| 	UI.Window.setParent(self) | ||||
| 	self:adjustWidth() | ||||
| end | ||||
|  | ||||
| function UI.StatusBar:setStatus(status) | ||||
| 	if self.values ~= status then | ||||
| 		self.values = status | ||||
| @@ -63,7 +52,7 @@ end | ||||
|  | ||||
| function UI.StatusBar:timedStatus(status, timeout) | ||||
| 	self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) | ||||
| 	Event.on(timeout or 3, function() | ||||
| 	Event.onTimeout(timeout or 3, function() | ||||
| 		if self.enabled then | ||||
| 			self:draw() | ||||
| 			self:sync() | ||||
| @@ -89,11 +78,13 @@ function UI.StatusBar:draw() | ||||
| 	elseif type(self.values) == 'string' then | ||||
| 		self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) | ||||
| 	else | ||||
| 		local s = '' | ||||
| 		local x = 2 | ||||
| 		self:clear() | ||||
| 		for _,c in ipairs(self.columns) do | ||||
| 			s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) | ||||
| 			local s = Util.widthify(tostring(self.values[c.key] or ''), c.cw) | ||||
| 			self:write(x, 1, s, c.bg, c.fg) | ||||
| 			x = x + c.cw + 1 | ||||
| 		end | ||||
| 		self:write(1, 1, Util.widthify(s, self.width)) | ||||
| 	end | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,16 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| UI.Tab = class(UI.ActiveLayer) | ||||
| UI.Tab = class(UI.Window) | ||||
| UI.Tab.defaults = { | ||||
| 	UIElement = 'Tab', | ||||
| 	tabTitle = 'tab', | ||||
| 	y = 2, | ||||
| } | ||||
|  | ||||
| function UI.Tab:draw() | ||||
| 	if not self.noFill then | ||||
| 		self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray) | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|   | ||||
| @@ -6,9 +6,9 @@ UI.TabBar = class(UI.MenuBar) | ||||
| UI.TabBar.defaults = { | ||||
| 	UIElement = 'TabBar', | ||||
| 	buttonClass = 'TabBarMenuItem', | ||||
| } | ||||
| UI.TabBar.inherits = { | ||||
| 	selectedBackgroundColor = 'backgroundColor', | ||||
| 	backgroundColor = 'black', | ||||
| 	selectedBackgroundColor = 'primary', | ||||
| 	unselectedBackgroundColor = 'tertiary', | ||||
| } | ||||
| function UI.TabBar:enable() | ||||
| 	UI.MenuBar.enable(self) | ||||
| @@ -32,7 +32,7 @@ function UI.TabBar:eventHandler(event) | ||||
| 				self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) | ||||
| 			end | ||||
| 		end | ||||
| 		UI.MenuBar.draw(self) | ||||
| 		self:draw(self) | ||||
| 	end | ||||
| 	return UI.MenuBar.eventHandler(self, event) | ||||
| end | ||||
|   | ||||
| @@ -1,27 +1,19 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.TabBarMenuItem = class(UI.Button) | ||||
| UI.TabBarMenuItem.defaults = { | ||||
| 	UIElement = 'TabBarMenuItem', | ||||
| 	event = 'tab_select', | ||||
| 	textColor = colors.black, | ||||
| 	selectedBackgroundColor = colors.cyan, | ||||
| 	unselectedBackgroundColor = colors.lightGray, | ||||
| 	backgroundColor = colors.lightGray, | ||||
| } | ||||
| UI.TabBarMenuItem.inherits = { | ||||
| 	selectedBackgroundColor = 'selectedBackgroundColor', | ||||
| 	textInactiveColor = 'lightGray', | ||||
| } | ||||
| function UI.TabBarMenuItem:draw() | ||||
| 	if self.selected then | ||||
| 		self.backgroundColor = self.selectedBackgroundColor | ||||
| 		self.backgroundFocusColor = self.selectedBackgroundColor | ||||
| 		self.backgroundColor = self:getProperty('selectedBackgroundColor') | ||||
| 		self.backgroundFocusColor = self.backgroundColor | ||||
| 	else | ||||
| 		self.backgroundColor = self.unselectedBackgroundColor | ||||
| 		self.backgroundFocusColor = self.unselectedBackgroundColor | ||||
| 		self.backgroundColor = self:getProperty('unselectedBackgroundColor') | ||||
| 		self.backgroundFocusColor = self.backgroundColor | ||||
| 	end | ||||
| 	UI.Button.draw(self) | ||||
| end | ||||
|   | ||||
| @@ -56,12 +56,12 @@ end | ||||
|  | ||||
| function UI.Tabs:enable() | ||||
| 	self.enabled = true | ||||
| 	self.transitionHint = nil | ||||
| 	self.tabBar:enable() | ||||
|  | ||||
| 	local menuItem = Util.find(self.tabBar.children, 'selected', true) | ||||
|  | ||||
| 	for _,child in pairs(self.children or { }) do | ||||
| 	for child in self:eachChild() do | ||||
| 		child.transitionHint = nil | ||||
| 		if child.uid == menuItem.tabUid then | ||||
| 			child:enable() | ||||
| 			self:emit({ type = 'tab_activate', activated = child }) | ||||
| @@ -74,14 +74,11 @@ end | ||||
| function UI.Tabs:eventHandler(event) | ||||
| 	if event.type == 'tab_change' then | ||||
| 		local tab = self:find(event.tab.tabUid) | ||||
| 		if event.current > event.last then | ||||
| 			self.transitionHint = 'slideLeft' | ||||
| 		else | ||||
| 			self.transitionHint = 'slideRight' | ||||
| 		end | ||||
| 		local hint = event.current > event.last and 'slideLeft' or 'slideRight' | ||||
|  | ||||
| 		for _,child in pairs(self.children) do | ||||
| 		for child in self:eachChild() do | ||||
| 			if child.uid == event.tab.tabUid then | ||||
| 				child.transitionHint = hint | ||||
| 				child:enable() | ||||
| 			elseif child.tabTitle then | ||||
| 				child:disable() | ||||
| @@ -89,6 +86,7 @@ function UI.Tabs:eventHandler(event) | ||||
| 		end | ||||
| 		self:emit({ type = 'tab_activate', activated = tab }) | ||||
| 		tab:draw() | ||||
| 		return true | ||||
| 	end | ||||
| end | ||||
|  | ||||
| @@ -102,11 +100,26 @@ function UI.Tabs.example() | ||||
| 		tab2 = UI.Tab { | ||||
| 			index = 2, | ||||
| 			tabTitle = 'tab2', | ||||
| 			button = UI.Button { y = 3 }, | ||||
| 			subtabs = UI.Tabs { | ||||
| 				x = 3, y = 2, ex = -3, ey = -2, | ||||
| 				tab1 = UI.Tab { | ||||
| 					index = 1, | ||||
| 					tabTitle = 'tab4', | ||||
| 					entry = UI.TextEntry { y = 3, shadowText = 'text' }, | ||||
| 				}, | ||||
| 				tab3 = UI.Tab { | ||||
| 					index = 2, | ||||
| 					tabTitle = 'tab5', | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		tab3 = UI.Tab { | ||||
| 			index = 3, | ||||
| 			tabTitle = 'tab3', | ||||
| 		}, | ||||
| 		enable = function(self) | ||||
| 			UI.Tabs.enable(self) | ||||
| 			self:setActive(self.tab3, false) | ||||
| 		end, | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -8,11 +8,11 @@ UI.Text.defaults = { | ||||
| 	value = '', | ||||
| 	height = 1, | ||||
| } | ||||
| function UI.Text:setParent() | ||||
| function UI.Text:layout() | ||||
| 	if not self.width and not self.ex then | ||||
| 		self.width = #tostring(self.value) | ||||
| 	end | ||||
| 	UI.Window.setParent(self) | ||||
| 	UI.Window.layout(self) | ||||
| end | ||||
|  | ||||
| function UI.Text:draw() | ||||
|   | ||||
| @@ -6,36 +6,44 @@ UI.TextArea.defaults = { | ||||
| 	UIElement = 'TextArea', | ||||
| 	marginRight = 2, | ||||
| 	value = '', | ||||
| 	showScrollBar = true, | ||||
| } | ||||
| function UI.TextArea:postInit() | ||||
| 	self.scrollBar = UI.ScrollBar() | ||||
| end | ||||
|  | ||||
| function UI.TextArea:setText(text) | ||||
| 	self:reset() | ||||
| 	self.value = text | ||||
| 	self:draw() | ||||
| end | ||||
|  | ||||
| function UI.TextArea:focus() | ||||
| function UI.TextArea.focus() | ||||
| 	-- allow keyboard scrolling | ||||
| end | ||||
|  | ||||
| function UI.TextArea:draw() | ||||
| 	self:clear() | ||||
| --  self:setCursorPos(1, 1) | ||||
| 	self.cursorX, self.cursorY = 1, 1 | ||||
| 	self:print(self.value) | ||||
|  | ||||
| 	for _,child in pairs(self.children) do | ||||
| 		if child.enabled then | ||||
| 			child:draw() | ||||
| 		end | ||||
| 	end | ||||
| 	self:drawChildren() | ||||
| end | ||||
|  | ||||
| function UI.TextArea.example() | ||||
| 	return UI.TextArea { | ||||
| 		value = 'sample text\nabc' | ||||
| 	local Ansi = require('opus.ansi') | ||||
| 	return UI.Window { | ||||
| 		backgroundColor = 2048, | ||||
| 		t1 = UI.TextArea { | ||||
| 			ey = 3, | ||||
| 			value = 'sample text\nabc' | ||||
| 		}, | ||||
| 		t2 = UI.TextArea { | ||||
| 			y = 5, | ||||
| 			backgroundColor = 'green', | ||||
| 			value = string.format([[now %%is the %stime %sfor%s all good men to come to the aid of their country. | ||||
| 1 | ||||
| 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | ||||
| 3 | ||||
| 4 | ||||
| 5 | ||||
| 6 | ||||
| 7 | ||||
| 8]], Ansi.yellow, Ansi.onred, Ansi.reset), | ||||
| 		} | ||||
| 	} | ||||
| end | ||||
| @@ -3,7 +3,6 @@ local entry = require('opus.entry') | ||||
| local UI    = require('opus.ui') | ||||
| local Util  = require('opus.util') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local _rep   = string.rep | ||||
|  | ||||
| local function transform(directive) | ||||
| @@ -19,15 +18,16 @@ UI.TextEntry = class(UI.Window) | ||||
| UI.TextEntry.docs = { } | ||||
| UI.TextEntry.defaults = { | ||||
| 	UIElement = 'TextEntry', | ||||
| 	--value = '', | ||||
| 	shadowText = '', | ||||
| 	focused = false, | ||||
| 	textColor = colors.white, | ||||
| 	shadowTextColor = colors.gray, | ||||
| 	backgroundColor = colors.black, -- colors.lightGray, | ||||
| 	backgroundFocusColor = colors.black, --lightGray, | ||||
| 	textColor = 'white', | ||||
| 	shadowTextColor = 'gray', | ||||
| 	markBackgroundColor = 'gray', | ||||
| 	backgroundColor = 'black', | ||||
| 	backgroundFocusColor = 'black', | ||||
| 	height = 1, | ||||
| 	limit = 6, | ||||
| 	cursorBlink = true, | ||||
| 	accelerators = { | ||||
| 		[ 'control-c' ] = 'copy', | ||||
| 	} | ||||
| @@ -74,7 +74,9 @@ function UI.TextEntry:draw() | ||||
| 		text = self.shadowText | ||||
| 	end | ||||
|  | ||||
| 	self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) | ||||
| 	local ss = self.entry.scroll > 0 and '\183' or ' ' | ||||
| 	self:write(2, 1, Util.widthify(text, self.width - 2) .. ' ', bg, tc) | ||||
| 	self:write(1, 1, ss, bg, self.shadowTextColor) | ||||
|  | ||||
| 	if self.entry.mark.active then | ||||
| 		local tx = math.max(self.entry.mark.x - self.entry.scroll, 0) | ||||
| @@ -85,7 +87,7 @@ function UI.TextEntry:draw() | ||||
| 		end | ||||
|  | ||||
| 		if tx ~= tex then | ||||
| 			self:write(tx + 2, 1, text:sub(tx + 1, tex), colors.gray, tc) | ||||
| 			self:write(tx + 2, 1, text:sub(tx + 1, tex), self.markBackgroundColor, tc) | ||||
| 		end | ||||
| 	end | ||||
| 	if self.focused then | ||||
| @@ -106,13 +108,12 @@ function UI.TextEntry:updateCursor() | ||||
| 	self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1) | ||||
| end | ||||
|  | ||||
| function UI.TextEntry:markAll() | ||||
| 	self.entry:markAll() | ||||
| end | ||||
|  | ||||
| function UI.TextEntry:focus() | ||||
| 	self:draw() | ||||
| 	if self.focused then | ||||
| 		self:setCursorBlink(true) | ||||
| 	else | ||||
| 		self:setCursorBlink(false) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.TextEntry:eventHandler(event) | ||||
|   | ||||
| @@ -20,24 +20,15 @@ UI.Throttle.defaults = { | ||||
| 		'  //)    (O ). @  \\-d )      (@ ' | ||||
| 	} | ||||
| } | ||||
| function UI.Throttle:setParent() | ||||
| function UI.Throttle:layout() | ||||
| 	self.x = math.ceil((self.parent.width - self.width) / 2) | ||||
| 	self.y = math.ceil((self.parent.height - self.height) / 2) | ||||
| 	UI.Window.setParent(self) | ||||
| 	self:reposition(self.x, self.y, self.width, self.height) | ||||
| end | ||||
|  | ||||
| function UI.Throttle:enable() | ||||
| 	self.c = os.clock() | ||||
| 	self.enabled = false | ||||
| end | ||||
|  | ||||
| function UI.Throttle:disable() | ||||
| 	if self.canvas then | ||||
| 		self.enabled = false | ||||
| 		self.canvas:removeLayer() | ||||
| 		self.canvas = nil | ||||
| 		self.ctr = 0 | ||||
| 	end | ||||
| 	self.ctr = 0 | ||||
| end | ||||
|  | ||||
| function UI.Throttle:update() | ||||
| @@ -46,11 +37,7 @@ function UI.Throttle:update() | ||||
| 		os.sleep(0) | ||||
| 		self.c = os.clock() | ||||
| 		self.enabled = true | ||||
| 		if not self.canvas then | ||||
| 			self.canvas = self:addLayer(self.backgroundColor, self.borderColor) | ||||
| 			self.canvas:setVisible(true) | ||||
| 			self:clear(self.borderColor) | ||||
| 		end | ||||
| 		self:clear(self.borderColor) | ||||
| 		local image = self.image[self.ctr + 1] | ||||
| 		local width = self.width - 2 | ||||
| 		for i = 0, #self.image do | ||||
| @@ -63,3 +50,25 @@ function UI.Throttle:update() | ||||
| 		self:sync() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Throttle.example() | ||||
| 	return UI.Window { | ||||
| 		button1 = UI.Button { | ||||
| 			x = 2, y = 2, | ||||
| 			text = 'Test', | ||||
| 		}, | ||||
| 		throttle = UI.Throttle { | ||||
| 			textColor = colors.yellow, | ||||
| 			borderColor = colors.green, | ||||
| 		}, | ||||
| 		eventHandler = function (self, event) | ||||
| 			if event.type == 'button_press' then | ||||
| 				for _ = 1, 40 do | ||||
| 					self.throttle:update() | ||||
| 					os.sleep(.05) | ||||
| 				end | ||||
| 				self.throttle:disable() | ||||
| 			end | ||||
| 		end, | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,59 +1,20 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
| local _rep   = string.rep | ||||
| local _sub   = string.sub | ||||
|  | ||||
| -- For manipulating text in a fixed width string | ||||
| local SB = class() | ||||
| function SB:init(width) | ||||
| 	self.width = width | ||||
| 	self.buf = _rep(' ', width) | ||||
| end | ||||
| function SB:insert(x, str, width) | ||||
| 	if x < 1 then | ||||
| 		x = self.width + x + 1 | ||||
| 	end | ||||
| 	width = width or #str | ||||
| 	if x + width - 1 > self.width then | ||||
| 		width = self.width - x | ||||
| 	end | ||||
| 	if width > 0 then | ||||
| 		self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width) | ||||
| 	end | ||||
| end | ||||
| function SB:fill(x, ch, width) | ||||
| 	width = width or self.width - x + 1 | ||||
| 	self:insert(x, _rep(ch, width)) | ||||
| end | ||||
| function SB:center(str) | ||||
| 	self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str) | ||||
| end | ||||
| function SB:get() | ||||
| 	return self.buf | ||||
| end | ||||
|  | ||||
| UI.TitleBar = class(UI.Window) | ||||
| UI.TitleBar.defaults = { | ||||
| 	UIElement = 'TitleBar', | ||||
| 	height = 1, | ||||
| 	textColor = colors.white, | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	title = '', | ||||
| 	frameChar = UI.extChars and '\140' or '-', | ||||
| 	closeInd = UI.extChars and '\215' or '*', | ||||
| } | ||||
| function UI.TitleBar:draw() | ||||
| 	local sb = SB(self.width) | ||||
| 	sb:fill(2, self.frameChar, sb.width - 3) | ||||
| 	sb:center(string.format(' %s ', self.title)) | ||||
| 	self:fillArea(2, 1, self.width - 2, 1, self.frameChar) | ||||
| 	self:centeredWrite(1, string.format(' %s ', self.title)) | ||||
| 	if self.previousPage or self.event then | ||||
| 		sb:insert(-1, self.closeInd) | ||||
| 	else | ||||
| 		sb:insert(-2, self.frameChar) | ||||
| 		self:write(self.width - 1, 1, ' ' .. self.closeInd) | ||||
| 	end | ||||
| 	self:write(1, 1, sb:get()) | ||||
| end | ||||
|  | ||||
| function UI.TitleBar:eventHandler(event) | ||||
| @@ -69,5 +30,74 @@ function UI.TitleBar:eventHandler(event) | ||||
| 			end | ||||
| 			return true | ||||
| 		end | ||||
|  | ||||
| 	elseif event.type == 'mouse_down' then | ||||
| 		self.anchor = { x = event.x, y = event.y, ox = self.parent.x, oy = self.parent.y, h = self.parent.height } | ||||
|  | ||||
| 	elseif event.type == 'mouse_drag' then | ||||
| 		if self.expand == 'height' then | ||||
| 			local d = event.dy | ||||
| 			if self.anchor.h - d > 0 and self.anchor.oy + d > 0 then | ||||
| 				self.parent:reposition(self.parent.x, self.anchor.oy + event.dy, self.width, self.anchor.h - d) | ||||
| 			end | ||||
|  | ||||
| 		elseif self.moveable then | ||||
| 			local d = event.dy | ||||
| 			if self.anchor.oy + d > 0 and self.anchor.oy + d <= self.parent.parent.height then | ||||
| 				self.parent:move(self.anchor.ox + event.dx, self.anchor.oy + event.dy) | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.TitleBar.example() | ||||
| 	return UI.Window { | ||||
| 		win1 = UI.Window { | ||||
| 			x = 9, y = 2, ex = -7, ey = -3, | ||||
| 			backgroundColor = 'green', | ||||
| 			titleBar = UI.TitleBar { | ||||
| 				title = 'A really, really, really long title',  moveable = true, | ||||
| 			}, | ||||
| 			button1 = UI.Button { | ||||
| 				x = 2, y = 3, | ||||
| 				text = 'Press', | ||||
| 			}, | ||||
| 			focus = function (self) | ||||
| 				self:raise() | ||||
| 			end, | ||||
| 		}, | ||||
| 		win2 = UI.Window { | ||||
| 			x = 7, y = 3, ex = -9, ey = -2, | ||||
| 			backgroundColor = 'orange', | ||||
| 			titleBar = UI.TitleBar { | ||||
| 				title = 'test', moveable = true, | ||||
| 				event = 'none', | ||||
| 			}, | ||||
| 			button1 = UI.Button { | ||||
| 				x = 2, y = 3, | ||||
| 				text = 'Press', | ||||
| 			}, | ||||
| 			focus = function (self) | ||||
| 				self:raise() | ||||
| 			end, | ||||
| 		}, | ||||
| 		draw = function(self, isBG) | ||||
| 			for i = 1, self.height do | ||||
| 				self:write(1, i, self.filler or '') | ||||
| 			end | ||||
| 			if not isBG then | ||||
| 				for _,v in pairs(self.children) do | ||||
| 					v:draw() | ||||
| 				end | ||||
| 			end | ||||
| 		end, | ||||
| 		enable = function (self) | ||||
| 			require('opus.event').onInterval(.5, function() | ||||
| 				self.filler = string.rep(string.char(math.random(33, 126)), self.width) | ||||
| 				self:draw(true) | ||||
| 				self:sync() | ||||
| 			end) | ||||
| 			UI.Window.enable(self) | ||||
| 		end | ||||
| 	} | ||||
| end | ||||
|   | ||||
| @@ -1,13 +1,11 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.VerticalMeter = class(UI.Window) | ||||
| UI.VerticalMeter.defaults = { | ||||
| 	UIElement = 'VerticalMeter', | ||||
| 	backgroundColor = colors.gray, | ||||
| 	meterColor = colors.lime, | ||||
| 	backgroundColor = 'gray', | ||||
| 	meterColor = 'lime', | ||||
| 	width = 1, | ||||
| 	value = 0, | ||||
| } | ||||
| @@ -18,12 +16,11 @@ function UI.VerticalMeter:draw() | ||||
| end | ||||
|  | ||||
| function UI.VerticalMeter.example() | ||||
| 	local Event = require('opus.event') | ||||
| 	return UI.VerticalMeter { | ||||
| 		x = 2, width = 3, y = 2, ey = -2, | ||||
| 		focus = function() end, | ||||
| 		enable = function(self) | ||||
| 			Event.onInterval(.25, function() | ||||
| 			require('opus.event').onInterval(.25, function() | ||||
| 				self.value = self.value == 100 and 0 or self.value + 5 | ||||
| 				self:draw() | ||||
| 				self:sync() | ||||
| @@ -31,4 +28,4 @@ function UI.VerticalMeter.example() | ||||
| 			return UI.VerticalMeter.enable(self) | ||||
| 		end | ||||
| 	} | ||||
| end | ||||
| end | ||||
|   | ||||
| @@ -1,16 +1,15 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.Viewport = class(UI.Window) | ||||
| UI.Viewport.defaults = { | ||||
| 	UIElement = 'Viewport', | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	accelerators = { | ||||
| 		down            = 'scroll_down', | ||||
| 		up              = 'scroll_up', | ||||
| 		home            = 'scroll_top', | ||||
| 		left            = 'scroll_left', | ||||
| 		right           = 'scroll_right', | ||||
| 		[ 'end' ]       = 'scroll_bottom', | ||||
| 		pageUp          = 'scroll_pageUp', | ||||
| 		[ 'control-b' ] = 'scroll_pageUp', | ||||
| @@ -18,53 +17,60 @@ UI.Viewport.defaults = { | ||||
| 		[ 'control-f' ] = 'scroll_pageDown', | ||||
| 	}, | ||||
| } | ||||
| function UI.Viewport:layout() | ||||
| 	UI.Window.layout(self) | ||||
| 	if not self.canvas then | ||||
| 		self.canvas = self:addLayer() | ||||
| 	else | ||||
| 		self.canvas:resize(self.width, self.height) | ||||
| function UI.Viewport:postInit() | ||||
| 	if self.showScrollBar then | ||||
| 		self.scrollBar = UI.ScrollBar() | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Viewport:enable() | ||||
| 	UI.Window.enable(self) | ||||
| 	self.canvas:setVisible(true) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:disable() | ||||
| 	UI.Window.disable(self) | ||||
| 	self.canvas:setVisible(false) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:setScrollPosition(offset) | ||||
| 	local oldOffset = self.offy | ||||
| 	self.offy = math.max(offset, 0) | ||||
| 	self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height) | ||||
| 	if self.offy ~= oldOffset then | ||||
| function UI.Viewport:setScrollPosition(offy, offx) -- argh - reverse | ||||
| 	local oldOffy = self.offy | ||||
| 	self.offy = math.max(offy, 0) | ||||
| 	self.offy = math.min(self.offy, math.max(#self.lines, self.height) - self.height) | ||||
| 	if self.offy ~= oldOffy then | ||||
| 		if self.scrollBar then | ||||
| 			self.scrollBar:draw() | ||||
| 		end | ||||
| 		self.canvas.offy = offset | ||||
| 		self.canvas:dirty() | ||||
| 		self.offy = offy | ||||
| 		self:dirty(true) | ||||
| 	end | ||||
|  | ||||
| 	local oldOffx = self.offx | ||||
| 	self.offx = math.max(offx or 0, 0) | ||||
| 	self.offx = math.min(self.offx, math.max(#self.lines[1], self.width) - self.width) | ||||
| 	if self.offx ~= oldOffx then | ||||
| 		if self.scrollBar then | ||||
| 			--self.scrollBar:draw() | ||||
| 		end | ||||
| 		self.offx = offx or 0 | ||||
| 		self:dirty(true) | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function UI.Viewport:write(x, y, text, bg, tc) | ||||
| 	if y > #self.canvas.lines then | ||||
| 		for i = #self.canvas.lines, y do | ||||
| 			self.canvas.lines[i + 1] = { } | ||||
| 			self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor) | ||||
| 		end | ||||
| function UI.Viewport:blit(x, y, text, bg, fg) | ||||
| 	if y > #self.lines then | ||||
| 		self:resizeBuffer(self.width, y) | ||||
| 	end | ||||
| 	return UI.Window.blit(self, x, y, text, bg, fg) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:write(x, y, text, bg, fg) | ||||
| 	if y > #self.lines then | ||||
| 		self:resizeBuffer(self.width, y) | ||||
| 	end | ||||
| 	return UI.Window.write(self, x, y, text, bg, fg) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:setViewHeight(h) | ||||
| 	if h > #self.lines then | ||||
| 		self:resizeBuffer(self.width, h) | ||||
| 	end | ||||
| 	return UI.Window.write(self, x, y, text, bg, tc) | ||||
| end | ||||
|  | ||||
| function UI.Viewport:reset() | ||||
| 	self.offy = 0 | ||||
| 	self.canvas.offy = 0 | ||||
| 	for i = self.height + 1, #self.canvas.lines do | ||||
| 		self.canvas.lines[i] = nil | ||||
| 	for i = self.height + 1, #self.lines do | ||||
| 		self.lines[i] = nil | ||||
| 	end | ||||
| end | ||||
|  | ||||
| @@ -72,26 +78,33 @@ function UI.Viewport:getViewArea() | ||||
| 	return { | ||||
| 		y           = (self.offy or 0) + 1, | ||||
| 		height      = self.height, | ||||
| 		totalHeight = #self.canvas.lines, | ||||
| 		totalHeight = #self.lines, | ||||
| 		offsetY     = self.offy or 0, | ||||
| 	} | ||||
| end | ||||
|  | ||||
| function UI.Viewport:eventHandler(event) | ||||
| 	if #self.lines <= self.height then | ||||
| 		return | ||||
| 	end | ||||
| 	if event.type == 'scroll_down' then | ||||
| 		self:setScrollPosition(self.offy + 1) | ||||
| 		self:setScrollPosition(self.offy + 1, self.offx) | ||||
| 	elseif event.type == 'scroll_up' then | ||||
| 		self:setScrollPosition(self.offy - 1) | ||||
| 		self:setScrollPosition(self.offy - 1, self.offx) | ||||
| 	elseif event.type == 'scroll_left' then | ||||
| 		self:setScrollPosition(self.offy, self.offx - 1) | ||||
| 	elseif event.type == 'scroll_right' then | ||||
| 		self:setScrollPosition(self.offy, self.offx + 1) | ||||
| 	elseif event.type == 'scroll_top' then | ||||
| 		self:setScrollPosition(0) | ||||
| 		self:setScrollPosition(0, 0) | ||||
| 	elseif event.type == 'scroll_bottom' then | ||||
| 		self:setScrollPosition(10000000) | ||||
| 		self:setScrollPosition(10000000, 0) | ||||
| 	elseif event.type == 'scroll_pageUp' then | ||||
| 		self:setScrollPosition(self.offy - self.height) | ||||
| 		self:setScrollPosition(self.offy - self.height, self.offx) | ||||
| 	elseif event.type == 'scroll_pageDown' then | ||||
| 		self:setScrollPosition(self.offy + self.height) | ||||
| 		self:setScrollPosition(self.offy + self.height, self.offx) | ||||
| 	elseif event.type == 'scroll_to' then | ||||
| 		self:setScrollPosition(event.offset) | ||||
| 		self:setScrollPosition(event.offset, 0) | ||||
| 	else | ||||
| 		return false | ||||
| 	end | ||||
|   | ||||
| @@ -25,9 +25,6 @@ function UI.Wizard:postInit() | ||||
| 	} | ||||
|  | ||||
| 	Util.merge(self, self.pages) | ||||
| 	--for _, child in pairs(self.pages) do | ||||
| 	--	child.ey = -2 | ||||
| 	--end | ||||
| end | ||||
|  | ||||
| function UI.Wizard:add(pages) | ||||
| @@ -50,9 +47,8 @@ end | ||||
| function UI.Wizard:enable(...) | ||||
| 	self.enabled = true | ||||
| 	self.index = 1 | ||||
| 	self.transitionHint = nil | ||||
| 	local initial = self:getPage(1) | ||||
| 	for _,child in pairs(self.children) do | ||||
| 	for child in self:eachChild() do | ||||
| 		if child == initial or not child.index then | ||||
| 			child:enable(...) | ||||
| 		else | ||||
| @@ -93,12 +89,13 @@ function UI.Wizard:eventHandler(event) | ||||
| 	elseif event.type == 'enable_view' then | ||||
| 		local current = event.next or event.prev | ||||
| 		if not current then error('property "index" is required on wizard pages') end | ||||
| 		local hint | ||||
|  | ||||
| 		if event.current then | ||||
| 			if event.next then | ||||
| 				self.transitionHint = 'slideLeft' | ||||
| 				hint = 'slideLeft' | ||||
| 			elseif event.prev then | ||||
| 				self.transitionHint = 'slideRight' | ||||
| 				hint = 'slideRight' | ||||
| 			end | ||||
| 			event.current:disable() | ||||
| 		end | ||||
| @@ -117,6 +114,7 @@ function UI.Wizard:eventHandler(event) | ||||
| 			self.nextButton.event = 'wizard_complete' | ||||
| 		end | ||||
| 		-- a new current view | ||||
| 		current.transitionHint = hint | ||||
| 		current:enable() | ||||
| 		current:emit({ type = 'view_enabled', view = current }) | ||||
| 		self:draw() | ||||
|   | ||||
| @@ -1,11 +1,8 @@ | ||||
| local class = require('opus.class') | ||||
| local UI    = require('opus.ui') | ||||
|  | ||||
| local colors = _G.colors | ||||
|  | ||||
| UI.WizardPage = class(UI.ActiveLayer) | ||||
| UI.WizardPage = class(UI.Window) | ||||
| UI.WizardPage.defaults = { | ||||
| 	UIElement = 'WizardPage', | ||||
| 	backgroundColor = colors.cyan, | ||||
| 	ey = -2, | ||||
| } | ||||
|   | ||||
| @@ -2,50 +2,85 @@ local Tween  = require('opus.ui.tween') | ||||
|  | ||||
| local Transition = { } | ||||
|  | ||||
| function Transition.slideLeft(args) | ||||
| 	local ticks      = args.ticks or 10 | ||||
| 	local easing     = args.easing or 'outQuint' | ||||
| 	local pos        = { x = args.ex } | ||||
| 	local tween      = Tween.new(ticks, pos, { x = args.x }, easing) | ||||
| function Transition.slideLeft(canvas, args) | ||||
| 	local ticks      = args.ticks or 6 | ||||
| 	local easing     = args.easing or 'inCirc' | ||||
| 	local pos        = { x = canvas.ex } | ||||
| 	local tween      = Tween.new(ticks, pos, { x = canvas.x }, easing) | ||||
|  | ||||
| 	args.canvas:move(pos.x, args.canvas.y) | ||||
| 	canvas:move(pos.x, canvas.y) | ||||
|  | ||||
| 	return function() | ||||
| 		local finished = tween:update(1) | ||||
| 		args.canvas:move(math.floor(pos.x), args.canvas.y) | ||||
| 		args.canvas:dirty() | ||||
| 		canvas:move(math.floor(pos.x), canvas.y) | ||||
| 		canvas:dirty(true) | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.slideRight(args) | ||||
| 	local ticks      = args.ticks or 10 | ||||
| 	local easing     = args.easing or'outQuint' | ||||
| 	local pos        = { x = -args.canvas.width } | ||||
| function Transition.slideRight(canvas, args) | ||||
| 	local ticks      = args.ticks or 6 | ||||
| 	local easing     = args.easing or 'inCirc' | ||||
| 	local pos        = { x = -canvas.width } | ||||
| 	local tween      = Tween.new(ticks, pos, { x = 1 }, easing) | ||||
|  | ||||
| 	args.canvas:move(pos.x, args.canvas.y) | ||||
| 	canvas:move(pos.x, canvas.y) | ||||
|  | ||||
| 	return function() | ||||
| 		local finished = tween:update(1) | ||||
| 		args.canvas:move(math.floor(pos.x), args.canvas.y) | ||||
| 		args.canvas:dirty() | ||||
| 		canvas:move(math.floor(pos.x), canvas.y) | ||||
| 		canvas:dirty(true) | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.expandUp(args) | ||||
| function Transition.expandUp(canvas, args) | ||||
| 	local ticks  = args.ticks or 3 | ||||
| 	local easing = args.easing or 'linear' | ||||
| 	local pos    = { y = args.ey + 1 } | ||||
| 	local tween  = Tween.new(ticks, pos, { y = args.y }, easing) | ||||
| 	local pos    = { y = canvas.ey + 1 } | ||||
| 	local tween  = Tween.new(ticks, pos, { y = canvas.y }, easing) | ||||
|  | ||||
| 	args.canvas:move(args.x, pos.y) | ||||
| 	canvas:move(canvas.x, pos.y) | ||||
|  | ||||
| 	return function() | ||||
| 		local finished = tween:update(1) | ||||
| 		args.canvas:move(args.x, math.floor(pos.y)) | ||||
| 		args.canvas:dirty() | ||||
| 		canvas:move(canvas.x, math.floor(pos.y)) | ||||
| 		canvas.parent:dirty(true) | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.shake(canvas, args) | ||||
| 	local ticks  = args.ticks or 8 | ||||
| 	local i = ticks | ||||
|  | ||||
| 	return function() | ||||
| 		i = -i | ||||
| 		canvas:move(canvas.x + i, canvas.y) | ||||
| 		if i > 0 then | ||||
| 			i = i - 2 | ||||
| 		end | ||||
| 		return i ~= 0 | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function Transition.shuffle(canvas, args) | ||||
| 	local ticks  = args.ticks or 4 | ||||
| 	local easing = args.easing or 'linear' | ||||
| 	local t = { } | ||||
|  | ||||
| 	for _,child in pairs(canvas.children) do | ||||
| 		t[child] = Tween.new(ticks, child, { x = child.x, y = child.y }, easing) | ||||
| 		child.x = math.random(1, canvas.parent.width) | ||||
| 		child.y = math.random(1, canvas.parent.height) | ||||
| 	end | ||||
|  | ||||
| 	return function() | ||||
| 		local finished | ||||
| 		for child, tween in pairs(t) do | ||||
| 			finished = tween:update(1) | ||||
| 			child:move(math.floor(child.x), math.floor(child.y)) | ||||
| 		end | ||||
| 		return not finished | ||||
| 	end | ||||
| end | ||||
|   | ||||
| @@ -13,6 +13,7 @@ local _unpack   = table.unpack | ||||
| local _bor      = bit32.bor | ||||
| local _bxor     = bit32.bxor | ||||
|  | ||||
| local byteArrayMT | ||||
| byteArrayMT = { | ||||
| 	__tostring = function(a) return string.char(_unpack(a)) end, | ||||
| 	__index = { | ||||
| @@ -668,42 +669,31 @@ function Util.trimr(s) | ||||
| end | ||||
| -- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76 | ||||
|  | ||||
| -- word wrapping based on: | ||||
| -- https://www.rosettacode.org/wiki/Word_wrap#Lua and | ||||
| -- http://lua-users.org/wiki/StringRecipes | ||||
| local function paragraphwrap(text, linewidth, res) | ||||
| 	linewidth = linewidth or 75 | ||||
| 	local spaceleft = linewidth | ||||
| 	local line = { } | ||||
|  | ||||
| 	for word in text:gmatch("%S+") do | ||||
| 		local len = #word + 1 | ||||
|  | ||||
| 		--if colorMode then | ||||
| 		--  word:gsub('()@([@%d])', function(pos, c) len = len - 2 end) | ||||
| 		--end | ||||
|  | ||||
| 		if len > spaceleft then | ||||
| 			table.insert(res, table.concat(line, ' ')) | ||||
| 			line = { word } | ||||
| 			spaceleft = linewidth - len - 1 | ||||
| local function wrap(text, max, lines) | ||||
| 	local index = 1 | ||||
| 	repeat | ||||
| 		if #text <= max then | ||||
| 			table.insert(lines, text) | ||||
| 			text = '' | ||||
| 		elseif text:sub(max+1, max+1) == ' ' then | ||||
| 			table.insert(lines, text:sub(index, max)) | ||||
| 			text = text:sub(max + 2) | ||||
| 		else | ||||
| 			table.insert(line, word) | ||||
| 			spaceleft = spaceleft - len | ||||
| 			local x = text:sub(1, max) | ||||
| 			local s = x:match('(.*) ') or x | ||||
| 			text = text:sub(#s + 1) | ||||
| 			table.insert(lines, s) | ||||
| 		end | ||||
| 	end | ||||
|  | ||||
| 	table.insert(res, table.concat(line, ' ')) | ||||
| 	return table.concat(res, '\n') | ||||
| 		text = text:match('^%s*(.*)') | ||||
| 	until not text or #text == 0 | ||||
| 	return lines | ||||
| end | ||||
| -- end word wrapping | ||||
|  | ||||
| function Util.wordWrap(str, limit) | ||||
| 	local longLines = Util.split(str) | ||||
| 	local lines = { } | ||||
|  | ||||
| 	for _,line in ipairs(longLines) do | ||||
| 		paragraphwrap(line, limit, lines) | ||||
| 	for _,line in ipairs(Util.split(str)) do | ||||
| 		wrap(line, limit, lines) | ||||
| 	end | ||||
|  | ||||
| 	return lines | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 kepler155c
					kepler155c