· 4 years ago · Jul 13, 2021, 10:22 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 term.setTextColor(32768)
289 term.setBackgroundColor(32768)
290 end
291 end
292 end
293 return true
294 end
295
296 function uiList:mouse(pos, tags, display)
297 local ut = self.utils
298 local mx, my = unpack(pos)
299 tags = tags or self.config.defTag
300 disp = display or self.config.defDis
301 clicked = {}
302 for key, value in ut.pairsByKey(self.ui) do
303 t, x, y, text, tag, display, enabled = ut.unpackPat(value,{'type', 'x', 'y', 'text', 'tag', 'display', 'enabled'})
304 if ut.inTable(tags, tag) > 0 and enabled and ut.inTable({'button','radio','checkbox','cycler'}, t) > 0 then
305 if type(text) == 'table' then
306 for i in ipairs(text) do
307 text[i] = ut.aRaw(text[i])
308 end
309 else
310 text = ut.aRaw(text)
311 end
312 local cont = value.cont
313 if cont and self.ui[cont] then
314 x = self.ui[cont].x + x
315 y = self.ui[cont].y + y
316 end
317 if type(text) == 'table' then
318 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
319 if my >= y and my <= y - 1 + #text and mx >= x and mx <= (x - 1 + textMax) then
320 table.insert(clicked, {value.ret, key, {mx + 1 - x, my + 1 - y}})
321 end
322 else
323 if t == 'radio' or t == 'checkbox' then custChar = value.custChar or {'O','0'} text = text:gsub('%%I%%', ut.noPat(custChar[value.value + 1])) end
324 if t == 'cycler' then
325 local val = value.value
326 local vals = value.values
327 local cyclers = value.cyclers
328 text = text:gsub('%%D%%',ut.noPat(cyclers[1]))
329 text = text:gsub('%%U%%',ut.noPat(cyclers[2]))
330 text = text:gsub('%%V%%',ut.noPat(vals[val]))
331 end
332 if my == y and mx >= x and mx <= (x + text:len() - 1) then
333 table.insert(clicked, {value.ret, key, {mx + 1 - x, my + 1 - y}})
334 end
335 end
336 end
337 end
338 if #clicked == 0 then
339 return nil
340 else
341 for i, v in pairs(clicked) do
342 local curList = self.ui[v[2]]
343 local text = curList.text
344 if type(text) == 'table' then
345 for i in ipairs(text) do
346 text[i] = ut.aRaw(text[i])
347 end
348 else
349 text = ut.aRaw(text)
350 end
351 if curList.type == 'radio' and curList.cont then
352 if curList.value == 0 then
353 grp = curList.cont
354 for ind, val in pairs(self.ui) do
355 if val['type'] == 'radio' and val.cont and val.cont == curList.cont then
356 if val.value ~= 0 then val.value = 0 end
357 end
358 end
359 curList.value = 1
360 end
361 if curList.exe and type(curList.exe) == 'function' then
362 curList:exe(self, v[3])
363 end
364 elseif curList.type == 'checkbox' then
365 if curList.value == 0 then
366 curList.value = 1
367 else
368 curList.value = 0
369 end
370 elseif curList.type == 'button' then
371 if curList.exe and type(curList.exe) == 'function' then
372 curList:exe(self, v[3])
373 end
374 elseif curList.type == 'cycler' then
375 -- '%D% %V% %U%'
376 -- '< 1 >'
377 -- '%D% 1 >'
378 -- '< 1 %U%'
379 -- '< %V% >'
380 local cyclers, val, vals = curList.cyclers, curList.value, curList.values
381
382 local doT = text:gsub('%%V%%',ut.noPat(vals[val])):gsub('%%U%%',ut.noPat(cyclers[2]))
383 local doS = doT:find('%%D%%')
384 local doE = doS + curList.cyclers[1]:len() - 1
385
386 local upT = text:gsub('%%D%%',ut.noPat(cyclers[1])):gsub('%%V%%',ut.noPat(vals[val]))
387 local upS = upT:find('%%U%%')
388 local upE = upS + curList.cyclers[2]:len() - 1
389
390 local teT = text:gsub('%%D%%',ut.noPat(cyclers[1])):gsub('%%U%%',ut.noPat(cyclers[2]))
391 local teS = teT:find('%%V%%')
392 local teE = teS + curList.values[curList.value]:len() - 1
393 local curE = curList.value
394 if upS and doS then
395 if curE == 0 then curE = 1 end
396 if v[3][1] >= upS and v[3][1] <= upE then
397 if curE < #curList.values then
398 curE = curE + 1
399 elseif curList.loop then
400 curE = 1
401 end
402 elseif v[3][1] >= doS and v[3][1] <= doE then
403 if curE > 1 then
404 curE = curE - 1
405 elseif curList.loop then
406 curE = #curList.values
407 end
408 elseif v[3][1] >= teS and v[3][1] <= teE then
409 if self.ui[v[2]].exe and type(self.ui[v[2]].exe) == 'function' then self.ui[v[2]]:exe(self, v[3]) end
410 end
411 curList.value = curE
412 end
413 end
414 end
415 return clicked
416 end
417 end
418
419 -- ################################################################################
420 --
421 -- Utils functions, contains the draw shapes 'n' stuff with some string utils
422 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
423 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
424 function uiList:setCommonAttribute(regEx, attrib)
425 for key, val in pairs(self.ui) do
426 local validate = true
427 local ut = self.utils
428 for i, v in pairs(regEx) do
429 if not(val[i]) or not(val[i]:find(v)) then
430 validate = false
431 break
432 end
433 end
434 if validate then
435 t = val.type
436 for i, v in pairs(attrib) do
437 if ut.inTable(self.config.pattern[t], i) > 0 then
438 val[i] = v
439 end
440 end
441 end
442 end
443 end
444 uiList.utils.aRaw = aRaw
445 uiList.utils.aWrite = aWrite
446 uiList.utils.noPat = noPat
447 uiList.utils.pairsByKey = pairsByKey
448 uiList.utils.draw = {}
449 uiList.utils.dbg = function(p) print( ' <> ',p,' <> ') sleep(2) end
450 uiList.utils.draw.line = line
451 uiList.utils.draw.box = box
452 uiList.utils.draw.outline = outline
453 uiList.utils.unpackPat = unpackPat
454 uiList.utils.cur = cur
455 uiList.utils.color = color
456 uiList.utils.split = split
457 uiList.utils.inTable = inTable
458 --
459 -- ################################################################################
460 return uiList
461end
462--
463-- End of new() constructor
464-- ################################################################################
465
466
467
468
469
470
471function drawOutline(x1,y1, x2, y2, target, c)
472 drawLine(x1,y1, x1, y2, target, c)
473 drawLine(x1,y2, x2, y2, target, c)
474 drawLine(x2,y1, x2, y2, target, c)
475 drawLine(x1,y1, x2, y1, target, c)
476end
477function outline(self, x1,y1, x2, y2, target, c)
478 self.line(x1,y1, x1, y2, target, c)
479 self.line(x1,y2, x2, y2, target, c)
480 self.line(x2,y1, x2, y2, target, c)
481 self.line(x1,y1, x2, y1, target, c)
482end
483
484-- function from paintutils ported for using targets
485local function drawPixelInternal( xPos, yPos, target )
486 target.setCursorPos(xPos, yPos)
487 target.write(" ")
488end
489
490-- function to draw a filled box on target
491function drawBox(startX, startY, endX, endY, target, nColour)
492 if startY <= endY then s = 1 else s= -1 end
493 for index=startY, endY, s do
494 drawLine( startX, index, endX, index, target, nColour )
495 end
496end
497function box(self, startX, startY, endX, endY, target, nColour)
498 if startY <= endY then s = 1 else s= -1 end
499 for index=startY, endY, s do
500 self.line( startX, index, endX, index, target, nColour )
501 end
502end
503
504-- function to draw line using targets
505function drawLine(startX, startY, endX, endY, target, nColour)
506 for i, v in pairs(target) do
507 line( startX, startY, endX, endY, v, nColour )
508 end
509end
510
511-- function from paintutils ported for using targets
512local function line(startX, startY, endX, endY, v, nColour )
513
514 local function drawPixelInternal( xPos, yPos, target ) target.setCursorPos(xPos, yPos) target.write(" ") end
515
516 if nColour then
517 v.setBackgroundColor( nColour )
518 end
519
520 startX = math.floor(startX)
521 startY = math.floor(startY)
522 endX = math.floor(endX)
523 endY = math.floor(endY)
524
525 if startX == endX and startY == endY then
526 drawPixelInternal( startX, startY, v )
527 return
528 end
529
530 local minX = math.min( startX, endX )
531 if minX == startX then
532 minY = startY
533 maxX = endX
534 maxY = endY
535 else
536 minY = endY
537 maxX = startX
538 maxY = startY
539 end
540
541 local xDiff = maxX - minX
542 local yDiff = maxY - minY
543
544 if xDiff > math.abs(yDiff) then
545 local y = minY
546 local dy = yDiff / xDiff
547 for x=minX,maxX do
548 drawPixelInternal( x, math.floor( y + 0.5 ), v )
549 y = y + dy
550 end
551 else
552 local x = minX
553 local dx = xDiff / yDiff
554 if maxY >= minY then
555 for y=minY,maxY do
556 drawPixelInternal( math.floor( x + 0.5 ), y, v )
557 x = x + dx
558 end
559 else
560 for y=minY,maxY,-1 do
561 drawPixelInternal( math.floor( x + 0.5 ), y, v )
562 x = x - dx
563 end
564 end
565 end
566end
567--
568-- ################################################################################