· 4 years ago · Jul 13, 2021, 02:34 PM
1-- ######################################## --
2-- --
3-- Better UI API for ComputerCraft v3 inDev --
4-- --
5-- ######################################## --
6
7-- ################################################################################
8--
9-- Base functions being used for many things
10function cur(target, x, y) target.setCursorPos(x, y) end
11function color(target,fg,bg) if fg ~= 0 then target.setTextColor(fg) end if bg ~= 0 then target.setBackgroundColor(bg) end end
12function drawPixel(target, x, y, nColour) if nColour then term.setBackgroundColor( nColour ) end cur(target, x, y) v.write(' ') end
13function split(se,sep) local sep, fields = sep or ":", {} local pattern = string.format("([^%s]+)", sep) se:gsub(pattern, function(c) fields[#fields+1] = c end) return fields end
14function aRaw(text)
15 return text:gsub('%%[fb][0123456789abcdef]','')
16end
17function aWrite(target, x, y, textRaw)
18 if y and textRaw then
19 target.setCursorPos(x, y)
20 else
21 textRaw = x
22 end
23 local cols = {}
24 for n=1,16 do
25 cols[string.sub( '0123456789abcdef',n,n ) ] = 2^(n-1)
26 end
27 local function color(t, c)
28 if t == 'f' then target.setTextColor(cols[c]) end
29 if t == 'b' then target.setBackgroundColor(cols[c]) end
30 end
31 local len = textRaw:len()
32 i = 1
33 while i ~= len + 1 do
34 if i < len - 1 and textRaw:sub(i, len):find('%%[fb][0123456789abcdef]') == 1 then
35 t, c = textRaw:sub(i, len):match('%%([fb])([0123456789abcdef])')
36 color(t, c)
37 i = i + 2
38 else
39 target.write(textRaw:sub(i,i))
40 end
41 i = i + 1
42 end
43end
44function inTable(tbl, item)
45 if type(item) == 'table' then
46 for i, v in pairs(item) do for key,value in pairs(tbl) do if value == v then return i, key end end end
47 elseif type(item) == 'string' then
48 for key,value in pairs(tbl) do if value == item then return key end end
49 end
50 return 0
51end
52function unpackPat(tbl, pattern)
53 local a = {}
54 for i, v in ipairs(pattern) do
55 a[i] = tbl[v]
56 end
57 return unpack(a)
58end
59function contPos(self,x,y,cont)
60 local loop = true
61 newx, newy = 0, 0
62 while loop do
63 newx = newx + self.ui[cont].x
64 newy = newy + self.ui[cont].y
65 if self.ui[cont].cont then
66 cont = self.ui[cont].cont
67 else
68 loop = false
69 end
70 return newx, newy
71 end
72end
73function pairsByKey(t, f)
74 local a = {}
75 for n in pairs(t) do table.insert(a, n) end
76 table.sort(a, f)
77 local i = 0 -- iterator variable
78 local iter = function () -- iterator function
79 i = i + 1
80 if a[i] == nil then return nil
81 else return a[i], t[a[i]]
82 end
83 end
84 return iter
85end
86function noPat(str)
87 local newStr = ''
88 local function inStr(s, c)
89 for i=1, s:len() do
90 if s:sub(i,i) == c then return true end
91 end
92 return false
93 end
94 for i=1, str:len() do
95 local char = str:sub(i,i)
96 if inStr('%-+^.[]()$?', char) then
97 newStr = newStr..'%'..char
98 else
99 newStr = newStr..char
100 end
101 end
102 return newStr
103end
104
105
106--
107-- ################################################################################
108
109-- ################################################################################
110-- New UI list constructor
111--
112function new(defTag, defDis)
113 local uiList = {}
114 uiList.ui = {}
115 uiList.set = {}
116 uiList.get = {}
117 uiList.utils = {}
118 uiList.config = {}
119 uiList.config.defDis = defDis or {term}
120 uiList.config.defTag = defTag or {'all'}
121 uiList.config.defFG = colors.white
122 uiList.config.defBG = colors.black
123 uiList.config.pattern = {} -- Defines the patterns the different element types use that can be edited (setCommonAttribute)
124 uiList.config.pattern.button = {'x', 'y', 'colors', 'tag', 'display', 'text', 'enabled', 'exe', 'ret', 'visible', 'cont'}
125 uiList.config.pattern.label = {'x', 'y', 'colors', 'tag', 'display', 'text', 'enabled', 'cont'}
126 uiList.config.pattern.radio = {'x', 'y', 'colors', 'tag', 'display', 'text', 'enabled', 'exe', 'ret', 'visible', 'value', 'cont'}
127 uiList.config.pattern.checkbox = {'x', 'y', 'colors', 'tag', 'display', 'text', 'enabled', 'exe', 'ret', 'visible', 'value', 'cont'}
128 uiList.config.pattern.point = {'x', 'y', 'colors', 'text', 'enabled', 'cont'}
129 uiList.config.pattern.cycler = {'x', 'y', 'colors', 'tag', 'display', 'enabled', 'exe', 'ret', 'visible', 'cont', 'values', 'loop'}
130 uiList.config.pattern.progress = {'x', 'y', 'w', 'colors', 'text', 'enabled', 'cont', 'value'}
131
132 function uiList:addPoint(key, x, y, enabled) -- Add a point used to mark position
133 if not(key) or not(x) or not(y) then return false end
134 self.ui[key] = {name = key, type = 'point', x = x, y = y, enabled = true}
135 local temp = self.ui[key]
136 function temp:setColors(fg,bg) self.colors = {fg, bg} end
137 return self.ui[key]
138 end
139
140 function uiList:addLabel(key, x, y, text, tag, display)
141 if not(key) or not(x) or not(y) or not(text) or self.ui[key] then return false end
142 self.ui[key] = {name = key, type = 'label', x = x, y = y, text = text, tag = tag or self.config.defTag, display = display or self.config.defDis, visible = true, enabled = true}
143 local temp = self.ui[key]
144 function temp:setColors(fg,bg) self.colors = {fg, bg} end
145 return self.ui[key]
146 end
147
148 function uiList:addButton(key, x, y, text, tag, display)
149 if not(key) or not(x) or not(y) or not(text) then return false end
150 self.ui[key] = {name = key, type = 'button', x = x, y = y, text = text, tag = tag or self.config.defTag, display = display or self.config.defDis, visible = true, enabled = true}
151 local temp = self.ui[key]
152 function temp:setColors(fg,bg) self.colors = {fg, bg} end
153 function temp:setAction(act) if type(act) == 'function' then self.exe = act else self.ret = act end end
154 return self.ui[key]
155 end
156
157 function uiList:addRadio(key, x, y, text, tag, display)
158 if not(key) or not(x) or not(y) or not(text) then return false end
159 self.ui[key] = {name = key, type = 'radio', x = x, y = y, text = text, tag = tag or self.config.defTag, display = display or self.config.defDis, visible = true, enabled = true, value = 0}
160 local temp = self.ui[key]
161 function temp:setColors(fg,bg) self.colors = {fg, bg} end
162 function temp:setAction(act) if type(act) == 'function' then self.exe = act else self.ret = act end end
163 function temp:setChar(disabled, enabled) self.custChar = {disabled,enabled} end
164 return self.ui[key]
165 end
166
167 function uiList:addCheckbox(key, x, y, text, tag, display)
168 if not(key) or not(x) or not(y) or not(text) then return false end
169 self.ui[key] = {name = key, type = 'checkbox', x = x, y = y, text = text, tag = tag or self.config.defTag, display = display or self.config.defDis, visible = true, enabled = true, value = 0}
170 local temp = self.ui[key]
171 function temp:setColors(fg,bg) self.colors = {fg, bg} end
172 function temp:setAction(act) if type(act) == 'function' then self.exe = act else self.ret = act end end
173 function temp:setChar(disabled, enabled) self.custChar = {disabled,enabled} end
174 return self.ui[key]
175 end
176
177 function uiList:addCycler(key, x, y, text, cyclers, values, tag, display)
178 if not(key) or not(x) or not(y) or not(text) or not(cyclers) or not(values) then return false end
179
180 --Parameter for values, if is string
181 if type(values) == 'string' then
182 local vals = values
183 local param, step, fill, max
184 local from, to = vals:match('([%d]+).([%d]+)')
185 if vals:find(':') then
186 param = vals:match(':(.+)')
187 if param:find('step=') ~= nil then step = tonumber(param:match('step=(%d+)')) end
188 if param:find('fill=') ~= nil then fill = param:match('fill=(.)') end
189 end
190 if not(step) then step = 1 end
191 if from > to then step = math.abs(step)*-1 else math.abs(step) end
192 values = {}
193 if fill then max = math.abs(#tostring(from) - #tostring(to)) + 1 end
194 for i=from, to, step do
195 local len = #tostring(i)
196 local str = tostring(i)
197 if fill and max - len > 0 then
198 table.insert(values, str..string.rep(fill, max - len))
199 else
200 table.insert(values, str)
201 end
202 end
203 end
204 --#####################################
205
206 self.ui[key] = {name = key, type = 'cycler', x = x, y = y, text = text, values = values, value = 1, loop = false, tag = tag or self.config.defTag, display = display or self.config.defDis, visible = true, enabled = true, cyclers = cyclers}
207 local temp = self.ui[key]
208 function temp:setColors(fg,bg) self.colors = {fg, bg} end
209 function temp:setAction(act) if type(act) == 'function' then self.exe = act else self.ret = act end end
210 function temp:getText() return self.values[self.value] end
211 return self.ui[key]
212 end
213
214 function uiList:addProgress(key, x, y, text, w, tag, display)
215 if not(key) or not(x) or not(y) or not(text) or not(w) then return false end
216 self.ui[key] = {name = key, type = 'progress', x = x, y = y, text = text, w = w, tag = tag or self.config.defTag, display = display or self.config.defDis, visible = true, enabled = true, value = 0}
217 local temp = self.ui[key]
218 function temp:setColors(fg,bg) self.colors = {fg, bg} end
219 function temp:progress(val) local w = self.w self.value = math.floor(val) end
220 return self.ui[key]
221 end
222
223 function uiList:del(key) self.ui[key] = nil return true end -- Working
224
225 function uiList:draw(tags)
226 local ut = self.utils
227 local tags = tags or self.config.tag or self.config.defTag
228 for key, value in ut.pairsByKey(self.ui) do
229 local t, x, y, text, tag, display, enabled= ut.unpackPat(value,{'type', 'x', 'y', 'text', 'tag', 'display', 'enabled'})
230 local visible = value.visible
231 if visible == nil then
232 local visible = true
233 end
234 local colors = value.colors
235 local cont = value.cont
236 local custChar = value.custChar
237 local val = value.value
238 local vals = value.values
239 local cyclers = value.cyclers
240 f, b = nil, nil
241 if colors then f, b = unpack(colors) end
242 if cont and self.ui[cont] then
243 x = self.ui[cont].x + x
244 y = self.ui[cont].y + y
245 if not(b) and self.ui[cont].colors then
246 b = self.ui[cont].colors[2]
247 end
248 enabled = self.ui[cont].enabled
249 end
250 fg = f or self.config.defFG
251 bg = b or self.config.defBG
252 if enabled and visible and t ~= 'point' and ut.inTable(tags, tag) > 0 then
253 if type(text) == 'table' then
254 local textMax = 0
255 for i, v in ipairs(text) do
256 v = ut.aRaw(v)
257 if type(v) ~= 'table' and v:len() > textMax then textMax = v:len() end
258 if type(v) == 'table' and v[1]:len() > textMax then textMax = v[1]:len() end
259 end
260 for i, v in ipairs(text) do
261 if type(v) ~= 'table' and ut.aRaw(v):len() < textMax then text[i] = text[i]..string.rep(' ', textMax - ut.aRaw(v):len()) end
262 end
263 end
264 for _, dis in pairs(display) do
265 ut.color(dis, fg, bg)
266 if type(text) == 'table' then
267 for i, v in ipairs(text) do
268 ut.color(dis, fg, bg)
269 ut.cur(dis, x, y + (i - 1))
270 ut.aWrite(dis,v)
271 end
272 else
273 if t == 'progress' and value.value > 0 then
274 text = value.text:sub(1,1):rep(math.floor(value.value / 100 * value.w))
275 end
276 if t == 'radio' or t == 'checkbox' then
277 custChar = custChar or {'O','0'}
278 if t == 'radio' or t == 'checkbox' then text = text:gsub('%%I%%', ut.noPat(custChar[val + 1])) end
279 end
280 if t == 'cycler' then
281 text = text:gsub('%%D%%',ut.noPat(cyclers[1]))
282 text = text:gsub('%%U%%',ut.noPat(cyclers[2]))
283 text = text:gsub('%%V%%',ut.noPat(vals[val]))
284 end
285 ut.cur(dis, x, y)
286 ut.aWrite(dis,text)
287 end
288 end
289 end
290 end
291 return true
292 end
293
294 function uiList:mouse(pos, tags, display)
295 local ut = self.utils
296 local mx, my = unpack(pos)
297 tags = tags or self.config.defTag
298 disp = display or self.config.defDis
299 clicked = {}
300 for key, value in ut.pairsByKey(self.ui) do
301 t, x, y, text, tag, display, enabled = ut.unpackPat(value,{'type', 'x', 'y', 'text', 'tag', 'display', 'enabled'})
302 if ut.inTable(tags, tag) > 0 and enabled and ut.inTable({'button','radio','checkbox','cycler'}, t) > 0 then
303 if type(text) == 'table' then
304 for i in ipairs(text) do
305 text[i] = ut.aRaw(text[i])
306 end
307 else
308 text = ut.aRaw(text)
309 end
310 local cont = value.cont
311 if cont and self.ui[cont] then
312 x = self.ui[cont].x + x
313 y = self.ui[cont].y + y
314 end
315 if type(text) == 'table' then
316 local textMax = 0 for _, v in ipairs(text) do if type(v) == 'table' then v = v[1] end if v:len() > textMax then textMax = v:len() end end
317 if my >= y and my <= y - 1 + #text and mx >= x and mx <= (x - 1 + textMax) then
318 table.insert(clicked, {value.ret, key, {mx + 1 - x, my + 1 - y}})
319 end
320 else
321 if t == 'radio' or t == 'checkbox' then custChar = value.custChar or {'O','0'} text = text:gsub('%%I%%', ut.noPat(custChar[value.value + 1])) end
322 if t == 'cycler' then
323 local val = value.value
324 local vals = value.values
325 local cyclers = value.cyclers
326 text = text:gsub('%%D%%',ut.noPat(cyclers[1]))
327 text = text:gsub('%%U%%',ut.noPat(cyclers[2]))
328 text = text:gsub('%%V%%',ut.noPat(vals[val]))
329 end
330 if my == y and mx >= x and mx <= (x + text:len() - 1) then
331 table.insert(clicked, {value.ret, key, {mx + 1 - x, my + 1 - y}})
332 end
333 end
334 end
335 end
336 if #clicked == 0 then
337 return nil
338 else
339 for i, v in pairs(clicked) do
340 local curList = self.ui[v[2]]
341 local text = curList.text
342 if type(text) == 'table' then
343 for i in ipairs(text) do
344 text[i] = ut.aRaw(text[i])
345 end
346 else
347 text = ut.aRaw(text)
348 end
349 if curList.type == 'radio' and curList.cont then
350 if curList.value == 0 then
351 grp = curList.cont
352 for ind, val in pairs(self.ui) do
353 if val['type'] == 'radio' and val.cont and val.cont == curList.cont then
354 if val.value ~= 0 then val.value = 0 end
355 end
356 end
357 curList.value = 1
358 end
359 if curList.exe and type(curList.exe) == 'function' then
360 curList:exe(self, v[3])
361 end
362 elseif curList.type == 'checkbox' then
363 if curList.value == 0 then
364 curList.value = 1
365 else
366 curList.value = 0
367 end
368 elseif curList.type == 'button' then
369 if curList.exe and type(curList.exe) == 'function' then
370 curList:exe(self, v[3])
371 end
372 elseif curList.type == 'cycler' then
373 -- '%D% %V% %U%'
374 -- '< 1 >'
375 -- '%D% 1 >'
376 -- '< 1 %U%'
377 -- '< %V% >'
378 local cyclers, val, vals = curList.cyclers, curList.value, curList.values
379
380 local doT = text:gsub('%%V%%',ut.noPat(vals[val])):gsub('%%U%%',ut.noPat(cyclers[2]))
381 local doS = doT:find('%%D%%')
382 local doE = doS + curList.cyclers[1]:len() - 1
383
384 local upT = text:gsub('%%D%%',ut.noPat(cyclers[1])):gsub('%%V%%',ut.noPat(vals[val]))
385 local upS = upT:find('%%U%%')
386 local upE = upS + curList.cyclers[2]:len() - 1
387
388 local teT = text:gsub('%%D%%',ut.noPat(cyclers[1])):gsub('%%U%%',ut.noPat(cyclers[2]))
389 local teS = teT:find('%%V%%')
390 local teE = teS + curList.values[curList.value]:len() - 1
391 local curE = curList.value
392 if upS and doS then
393 if curE == 0 then curE = 1 end
394 if v[3][1] >= upS and v[3][1] <= upE then
395 if curE < #curList.values then
396 curE = curE + 1
397 elseif curList.loop then
398 curE = 1
399 end
400 elseif v[3][1] >= doS and v[3][1] <= doE then
401 if curE > 1 then
402 curE = curE - 1
403 elseif curList.loop then
404 curE = #curList.values
405 end
406 elseif v[3][1] >= teS and v[3][1] <= teE then
407 if self.ui[v[2]].exe and type(self.ui[v[2]].exe) == 'function' then self.ui[v[2]]:exe(self, v[3]) end
408 end
409 curList.value = curE
410 end
411 end
412 end
413 return clicked
414 end
415 end
416
417 -- ################################################################################
418 --
419 -- Utils functions, contains the draw shapes 'n' stuff with some string utils
420 function uiList:getRadio(cont) for i, v in pairs(self.ui) do if v.type == 'radio' and v.value == 1 and v.cont == cont then return v end end return nil end
421 function uiList:getCheckbox(cont) local res = {} for i, v in pairs(self.ui) do if v.type == 'checkbox' and v.value == 1 and v.cont == cont then table.insert(res, v) end end if #res > 0 then return res else return nil end end
422 function uiList:setCommonAttribute(regEx, attrib)
423 for key, val in pairs(self.ui) do
424 local validate = true
425 local ut = self.utils
426 for i, v in pairs(regEx) do
427 if not(val[i]) or not(val[i]:find(v)) then
428 validate = false
429 break
430 end
431 end
432 if validate then
433 t = val.type
434 for i, v in pairs(attrib) do
435 if ut.inTable(self.config.pattern[t], i) > 0 then
436 val[i] = v
437 end
438 end
439 end
440 end
441 end
442 uiList.utils.aRaw = aRaw
443 uiList.utils.aWrite = aWrite
444 uiList.utils.noPat = noPat
445 uiList.utils.pairsByKey = pairsByKey
446 uiList.utils.draw = {}
447 uiList.utils.dbg = function(p) print( ' <> ',p,' <> ') sleep(2) end
448 uiList.utils.draw.line = line
449 uiList.utils.draw.box = box
450 uiList.utils.draw.outline = outline
451 uiList.utils.unpackPat = unpackPat
452 uiList.utils.cur = cur
453 uiList.utils.color = color
454 uiList.utils.split = split
455 uiList.utils.inTable = inTable
456 --
457 -- ################################################################################
458 return uiList
459end
460--
461-- End of new() constructor
462-- ################################################################################
463
464
465
466
467
468
469function drawOutline(x1,y1, x2, y2, target, c)
470 drawLine(x1,y1, x1, y2, target, c)
471 drawLine(x1,y2, x2, y2, target, c)
472 drawLine(x2,y1, x2, y2, target, c)
473 drawLine(x1,y1, x2, y1, target, c)
474end
475function outline(self, x1,y1, x2, y2, target, c)
476 self.line(x1,y1, x1, y2, target, c)
477 self.line(x1,y2, x2, y2, target, c)
478 self.line(x2,y1, x2, y2, target, c)
479 self.line(x1,y1, x2, y1, target, c)
480end
481
482-- function from paintutils ported for using targets
483local function drawPixelInternal( xPos, yPos, target )
484 target.setCursorPos(xPos, yPos)
485 target.write(" ")
486end
487
488-- function to draw a filled box on target
489function drawBox(startX, startY, endX, endY, target, nColour)
490 if startY <= endY then s = 1 else s= -1 end
491 for index=startY, endY, s do
492 drawLine( startX, index, endX, index, target, nColour )
493 end
494end
495function box(self, startX, startY, endX, endY, target, nColour)
496 if startY <= endY then s = 1 else s= -1 end
497 for index=startY, endY, s do
498 self.line( startX, index, endX, index, target, nColour )
499 end
500end
501
502-- function to draw line using targets
503function drawLine(startX, startY, endX, endY, target, nColour)
504 for i, v in pairs(target) do
505 line( startX, startY, endX, endY, v, nColour )
506 end
507end
508
509-- function from paintutils ported for using targets
510local function line(startX, startY, endX, endY, v, nColour )
511
512 local function drawPixelInternal( xPos, yPos, target ) target.setCursorPos(xPos, yPos) target.write(" ") end
513
514 if nColour then
515 v.setBackgroundColor( nColour )
516 end
517
518 startX = math.floor(startX)
519 startY = math.floor(startY)
520 endX = math.floor(endX)
521 endY = math.floor(endY)
522
523 if startX == endX and startY == endY then
524 drawPixelInternal( startX, startY, v )
525 return
526 end
527
528 local minX = math.min( startX, endX )
529 if minX == startX then
530 minY = startY
531 maxX = endX
532 maxY = endY
533 else
534 minY = endY
535 maxX = startX
536 maxY = startY
537 end
538
539 local xDiff = maxX - minX
540 local yDiff = maxY - minY
541
542 if xDiff > math.abs(yDiff) then
543 local y = minY
544 local dy = yDiff / xDiff
545 for x=minX,maxX do
546 drawPixelInternal( x, math.floor( y + 0.5 ), v )
547 y = y + dy
548 end
549 else
550 local x = minX
551 local dx = xDiff / yDiff
552 if maxY >= minY then
553 for y=minY,maxY do
554 drawPixelInternal( math.floor( x + 0.5 ), y, v )
555 x = x + dx
556 end
557 else
558 for y=minY,maxY,-1 do
559 drawPixelInternal( math.floor( x + 0.5 ), y, v )
560 x = x - dx
561 end
562 end
563 end
564end
565--
566-- ################################################################################