Computercraft thingies
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

graphics.lua 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. os.loadAPI("/lib/util")
  2. function prettyPrintSize(tokens)
  3. if type(tokens) == "number" then
  4. return tostring(tokens)
  5. end
  6. local str = "( "
  7. for key, token in pairs(tokens) do
  8. if token.tokentype == "list" then
  9. str = str .. prettyPrintSize(token)
  10. elseif token.tokentype == "num" then
  11. str = str .. token.val .. " "
  12. elseif token.tokentype == "percent" then
  13. str = str .. token.val .. "% "
  14. elseif token.tokentype == "selfpercent" then
  15. str = str .. token.val .. "$ "
  16. elseif token.tokentype == "auto" then
  17. str = str .. "& "
  18. elseif token.tokentype == "add" then
  19. str = str .. "+ "
  20. elseif token.tokentype == "sub" then
  21. str = str .. "- "
  22. elseif token.tokentype == "multiply" then
  23. str = str .. "* "
  24. elseif token.tokentype == "divide" then
  25. str = str .. "/ "
  26. end
  27. end
  28. return str .. ")"
  29. end
  30. function parseSize(size)
  31. if type(size) == "number" then
  32. return size
  33. end
  34. size = size .. " "
  35. size = string.gsub(size, "center", "50%% - 50$")
  36. print(size)
  37. local res = {
  38. tokentype = "list"
  39. }
  40. local current = ""
  41. local parsing = true
  42. local i = 1
  43. while i <= #size do
  44. local c = string.sub(size, i, i)
  45. if tonumber(c) ~= nil then
  46. current = current .. c
  47. elseif current ~= "" then
  48. local cur = tonumber(current)
  49. if cur == nil or current == "+" or current == "-" then
  50. -- Don't do anything because there's no number
  51. elseif c == "%" then
  52. table.insert(res, {
  53. tokentype = "percent";
  54. val = cur;
  55. })
  56. elseif c == "$" then
  57. table.insert(res, {
  58. tokentype = "selfpercent";
  59. val = cur;
  60. })
  61. else
  62. table.insert(res, {
  63. tokentype = "num";
  64. val = cur;
  65. })
  66. end
  67. current = ""
  68. end
  69. if c == "(" then
  70. local substr = ""
  71. local depth = 1
  72. while depth > 0 do
  73. i = i + 1
  74. local cc = string.sub(size, i, i)
  75. if cc == "(" then
  76. depth = depth + 1
  77. elseif cc == ")" then
  78. depth = depth - 1
  79. end
  80. if depth > 0 then
  81. substr = substr .. cc
  82. end
  83. end
  84. table.insert(res, parseSize(substr))
  85. elseif c == "&" then
  86. table.insert(res, {
  87. tokentype = "auto"
  88. })
  89. elseif c == "+" then
  90. table.insert(res, {
  91. tokentype = "add"
  92. })
  93. elseif c == "-" then
  94. table.insert(res, {
  95. tokentype = "sub"
  96. })
  97. elseif c == "*" then
  98. table.insert(res, {
  99. tokentype = "multiply"
  100. })
  101. elseif c == "/" then
  102. table.insert(res, {
  103. tokentype = "divide"
  104. })
  105. end
  106. i = i + 1
  107. end
  108. return res
  109. end
  110. function calcSize(tokens, parentSizeArg, selfSizeArg, autoSizeArg)
  111. if type(tokens) == "number" then
  112. return tokens
  113. elseif tokens == nil then
  114. return nil
  115. end
  116. local parentSize = 0
  117. if type(parentSizeArg) == "function" then
  118. parentSize = parentSizeArg()
  119. elseif type(parentSizeArg) == "number" then
  120. parentSize = parentSizeArg
  121. end
  122. local selfSize = 0
  123. if type(selfSizeArg) == "function" then
  124. selfSize = selfSizeArg()
  125. elseif type(selfSizeArg) == "number" then
  126. selfSize = selfSizeArg
  127. end
  128. local autoSize = 0
  129. if type(autoSizeArg) == "function" then
  130. autoSize = autoSizeArg()
  131. elseif type(autoSizeArg) == "number" then
  132. autoSize = autoSizeArg
  133. end
  134. function getVal(token)
  135. if token.tokentype == "num" then
  136. return token.val
  137. elseif token.tokentype == "percent" then
  138. return util.percentOf(token.val, parentSize)
  139. elseif token.tokentype == "selfpercent" then
  140. return util.percentOf(token.val, selfSize) - 1
  141. elseif token.tokentype == "auto" then
  142. return autoSize
  143. elseif token.tokentype == "list" then
  144. return calcSize(token, parentSize, selfSize)
  145. else
  146. return 0
  147. end
  148. end
  149. local sum = 0
  150. local opr = "add"
  151. local i = 1
  152. while i <= #tokens do
  153. local num = getVal(tokens[i])
  154. if opr == "add" then
  155. sum = sum + num
  156. elseif opr == "sub" then
  157. sum = sum - num
  158. elseif opr == "multiply" then
  159. sum = sum * num
  160. elseif opr == "divide" then
  161. sum = sum / num
  162. else
  163. error("Unexpected operator: "..opr)
  164. return 0
  165. end
  166. local oprToken = tokens[i + 1]
  167. if oprToken ~= nil then
  168. opr = oprToken.tokentype
  169. else
  170. opr = ""
  171. end
  172. i = i + 2
  173. end
  174. return math.floor(sum)
  175. end
  176. function Widget()
  177. local self = util.newObject()
  178. util.makeEventListener(self)
  179. local prog = nil
  180. local focused = false
  181. local children = {}
  182. local parent = nil
  183. local top = 1
  184. local bottom = nil
  185. local left = 1
  186. local right = nil
  187. local width = parseSize("&")
  188. local height = parseSize("&")
  189. local background = "black"
  190. local foreground = "white"
  191. local x = 1
  192. local y = 1
  193. self.getset("prog",
  194. function() return prog end,
  195. function(val) prog = val end)
  196. self.getset("focused",
  197. function() return focused end)
  198. self.getset("children",
  199. function() return children end)
  200. self.getset("parent",
  201. function() return parent end)
  202. self.getset("_parent",
  203. function() return parent end,
  204. function(val) parent = val end)
  205. self.getset("width",
  206. function() return calcSize(width, parent.width, 0, self.autoWidth) end,
  207. function(val) width = parseSize(val) end)
  208. self.getset("height",
  209. function() return calcSize(height, parent.height, 0, self.autoHeight) end,
  210. function(val) height = parseSize(val) end)
  211. self.getset("background",
  212. function() return background end,
  213. function(val) background = val end)
  214. self.getset("foreground",
  215. function() return foreground end,
  216. function(val) foreground = val end)
  217. self.getset("top",
  218. function() return calcSize(top, parent.height, self.height, 0) end,
  219. function(val) top = parseSize(val) end)
  220. self.getset("bottom",
  221. function() return calcSize(bottom, parent.height, self.height, 0) end,
  222. function(val) bottom = parseSize(val) end)
  223. self.getset("left",
  224. function() return calcSize(left, parent.width, self.width, 0) end,
  225. function(val) left = parseSize(val) end)
  226. self.getset("right",
  227. function() return calcSize(right, parent.width, self.width, 0) end,
  228. function(val) right = parseSize(val) end)
  229. self.getset("x",
  230. function() return x end,
  231. function(val) x = val end)
  232. self.getset("y",
  233. function() return y end,
  234. function(val) y = val end)
  235. self.focus = function()
  236. focused = true
  237. self.emit("focus", self)
  238. end
  239. self.unfocus = function()
  240. focused = false
  241. self.emit("unfocus", self)
  242. end
  243. self.onclick = function(x, y)
  244. for key, child in pairs(self.children()) do
  245. local cx = child.x()
  246. local cy = child.y()
  247. local cw = child.width()
  248. local ch = child.height()
  249. if x >= cx and x < cx + cw
  250. and y >= cy and y < cy + ch then
  251. child.onclick(x, y)
  252. end
  253. end
  254. end
  255. self.onchar = function(char) end
  256. self.onkey = function(key) end
  257. self.setColors = function(screen)
  258. screen.setTextColor(colors[foreground])
  259. screen.setBackgroundColor(colors[background])
  260. local w = self.width()
  261. local h = self.height()
  262. local str = ""
  263. for i = 1, w do
  264. str = str .. " "
  265. end
  266. local x = self.x()
  267. local y = self.y()
  268. for i = 1, h do
  269. screen.setCursorPos(x, y + i - 1)
  270. screen.write(str)
  271. end
  272. screen.setCursorPos(x, y)
  273. end
  274. self.addChild = function(widget)
  275. if widget.parent() ~= nil then
  276. error("Widget already has a parent")
  277. return
  278. end
  279. table.insert(children, widget)
  280. widget._parent(self)
  281. widget.prog(prog)
  282. widget.on("mustreflow", function()
  283. self.emit("mustreflow")
  284. end)
  285. widget.on("mustredraw", function(elem)
  286. self.emit("mustredraw", elem)
  287. end)
  288. widget.on("focus", function(elem)
  289. self.emit("focus", elem)
  290. end)
  291. widget.on("unfocus", function(elem)
  292. self.emit("unfocus", elem)
  293. end)
  294. self.emit("mustreflow")
  295. end
  296. self.removeChild = function(child)
  297. local removed = false
  298. for key, val in pairs(children) do
  299. if val == child then
  300. removed = true
  301. child.unfocus()
  302. table.remove(children, key)
  303. end
  304. end
  305. if removed then
  306. self.emit("mustreflow")
  307. end
  308. end
  309. self.reflow = util.abstract
  310. self.draw = util.abstract
  311. return self
  312. end
  313. function LinearLayout()
  314. local self = Widget()
  315. local orientation = "vertical"
  316. local reversed = false
  317. self.getset("orientation",
  318. function() return orientation end,
  319. function(val) orientation = val end)
  320. self.getset("reversed",
  321. function() return reversed end,
  322. function(val) reversed = val end)
  323. self.draw = function(screen)
  324. self.setColors(screen)
  325. for key, child in pairs(self.children()) do
  326. child.draw(screen)
  327. end
  328. end
  329. self.reflow = function(screen, x, y)
  330. self.x(x)
  331. self.y(y)
  332. local selfw = self.width()
  333. local selfh = self.height()
  334. local offset = 0
  335. for key, child in pairs(self.children()) do
  336. local cx = x
  337. local cy = y
  338. local height = child.height()
  339. local width = child.width()
  340. local top = child.top()
  341. local bottom = child.bottom()
  342. local left = child.left()
  343. local right = child.right()
  344. if right ~= nil then
  345. cx = cx +
  346. selfw -
  347. width -
  348. right + 1
  349. else
  350. cx = cx + top - 1
  351. end
  352. if bottom ~= nil then
  353. cy = cy +
  354. selfh -
  355. height -
  356. bottom + 1
  357. else
  358. cy = cy + top - 1
  359. end
  360. if orientation == "vertical" then
  361. cy = y + offset + top - 1
  362. offset = offset +
  363. top - 1 +
  364. height +
  365. (bottom or 1) - 1
  366. if reversed then
  367. cy = selfh - cy - height + 2
  368. end
  369. elseif orientation == "horizontal" then
  370. cx = x + offset + left - 1
  371. offset = offset +
  372. left - 1 +
  373. width +
  374. (right or 1) - 1
  375. if reversed then
  376. cx = selfw - cx - width + 2
  377. end
  378. end
  379. child.reflow(screen, cx, cy)
  380. end
  381. end
  382. return self
  383. end
  384. function FloatingLayout()
  385. local self = Widget()
  386. self.draw = function(screen)
  387. self.setColors(screen)
  388. for key, child in pairs(self.children()) do
  389. child.draw(screen)
  390. end
  391. end
  392. self.reflow = function(screen, x, y)
  393. self.x(x)
  394. self.y(y)
  395. for key, child in pairs(self.children()) do
  396. local cx = x
  397. local cy = y
  398. if child.right() ~= nil then
  399. cx = cx +
  400. self.width() -
  401. child.width() -
  402. child.right() + 1
  403. else
  404. cx = cx + child.left() - 1
  405. end
  406. if child.bottom() ~= nil then
  407. cy = cy +
  408. self.height() -
  409. child.height() -
  410. child.bottom() + 1
  411. else
  412. cy = cy + child.top() - 1
  413. end
  414. child.reflow(screen, cx, cy)
  415. end
  416. end
  417. self.autoWidth = function()
  418. return self.parent().width()
  419. end
  420. self.autoHeight = function()
  421. return self.parent().height()
  422. end
  423. return self
  424. end
  425. function TextView()
  426. local self = Widget()
  427. local text = ""
  428. self.getset("text",
  429. function() return text end,
  430. function(val)
  431. local pre = text
  432. text = tostring(val)
  433. if #pre == #text then
  434. self.emit("mustredraw", self)
  435. else
  436. self.emit("mustreflow")
  437. end
  438. end)
  439. self.reflow = function(screen, x, y)
  440. self.x(x)
  441. self.y(y)
  442. end
  443. self.draw = function(screen)
  444. self.setColors(screen)
  445. screen.setCursorPos(self.x(), self.y())
  446. local w = self.width()
  447. local h = self.height()
  448. local offy = math.floor(h / 2)
  449. local offx = math.floor(w - #text - ((w - #text) / 2))
  450. screen.setCursorPos(self.x() + offx, self.y() + offy)
  451. screen.write(text)
  452. end
  453. self.autoWidth = function()
  454. return #text
  455. end
  456. self.autoHeight = function()
  457. return 1
  458. end
  459. self.onclick = function(x, y)
  460. -- TextViews don't have any interesting onclick actions
  461. end
  462. return self
  463. end
  464. function TextInput()
  465. local self = Widget()
  466. local backgroundInactive = "blue"
  467. local backgroundActive = "red"
  468. local foregroundInactive = "white"
  469. local foregroundActive = "white"
  470. local key_backspace = 14
  471. local key_left = 203
  472. local key_right = 205
  473. local key_delete = 211
  474. local text = ""
  475. local cursor = 0
  476. self.getset("text",
  477. function() return text end,
  478. function(val)
  479. local pre = text
  480. text = tostring(val)
  481. if #pre == #text then
  482. self.emit("mustredraw", self)
  483. else
  484. self.emit("mustreflow")
  485. end
  486. end)
  487. self.getset("backgroundInactive",
  488. function() return backgroundInactive end,
  489. function(val) backgroundInactive = val end)
  490. self.getset("backgroundActive",
  491. function() return backgroundActive end,
  492. function(val) backgroundActive = val end)
  493. self.getset("foregroundInactive",
  494. function() return foregroundInactive end,
  495. function(val) foregroundInactive = val end)
  496. self.getset("foregroundActive",
  497. function() return foregroundActive end,
  498. function(val) foregroundActive = val end)
  499. self.reflow = function(screen, x, y)
  500. self.x(x)
  501. self.y(y)
  502. end
  503. self.draw = function(screen)
  504. if self.focused() then
  505. self.background(backgroundActive)
  506. self.foreground(foregroundActive)
  507. else
  508. self.background(backgroundInactive)
  509. self.foreground(foregroundInactive)
  510. end
  511. self.setColors(screen)
  512. screen.setCursorPos(self.x(), self.y())
  513. screen.write("|")
  514. screen.setCursorPos(self.x() + self.width() - 1, self.y())
  515. screen.write("|")
  516. local len = self.width() - 2
  517. screen.setCursorPos(self.x() + 1, self.y())
  518. screen.write(string.sub(text, 1, len))
  519. if self.focused() then
  520. screen.setCursorPos(self.x() + 1 + cursor, self.y())
  521. screen.setBackgroundColor(colors.white)
  522. screen.write(" ")
  523. end
  524. end
  525. self.autoWidth = function()
  526. return #text + 2
  527. end
  528. self.autoHeight = function()
  529. return 1
  530. end
  531. self.onclick = function(x, y)
  532. local w = self.width()
  533. if x == self.x() or x == w then
  534. cursor = #text
  535. else
  536. cursor = math.min(x - self.x() - 1, #text)
  537. end
  538. self.focus()
  539. end
  540. self.onchar = function(char)
  541. local pre = string.sub(text, 1, cursor)
  542. local post = string.sub(text, cursor + 1, #text)
  543. text = pre .. char .. post
  544. cursor = cursor + 1
  545. self.emit("mustreflow")
  546. end
  547. self.onkey = function(key)
  548. if key == key_backspace then
  549. local pre = string.sub(text, 1, cursor - 1)
  550. local post = string.sub(text, cursor + 1, #text)
  551. text = pre .. post
  552. if cursor > 0 then
  553. cursor = cursor - 1
  554. end
  555. self.emit("mustreflow")
  556. elseif key == key_delete then
  557. local pre = string.sub(text, 1, cursor)
  558. local post = string.sub(text, cursor + 2, #text)
  559. text = pre .. post
  560. self.emit("mustreflow")
  561. elseif key == key_left then
  562. if cursor > 0 then
  563. cursor = cursor - 1
  564. end
  565. self.emit("mustredraw", self)
  566. elseif key == key_right then
  567. if cursor < #text then
  568. cursor = cursor + 1
  569. end
  570. self.emit("mustredraw", self)
  571. end
  572. end
  573. self.on("focus", function() self.emit("mustredraw", self) end)
  574. self.on("unfocus", function() self.emit("mustredraw", self) end)
  575. return self
  576. end
  577. function Button()
  578. local self = TextView()
  579. local backgroundInactive = "blue"
  580. local backgroundActive = "red"
  581. local foregroundInactive = "white"
  582. local foregroundActive = "white"
  583. self.getset("backgroundInactive",
  584. function() return backgroundInactive end,
  585. function(val) backgroundInactive = val end)
  586. self.getset("backgroundActive",
  587. function() return backgroundActive end,
  588. function(val) backgroundActive = val end)
  589. self.getset("foregroundInactive",
  590. function() return foregroundInactive end,
  591. function(val) foregroundInactive = val end)
  592. self.getset("foregroundActive",
  593. function() return foregroundActive end,
  594. function(val) foregroundActive = val end)
  595. self.background(backgroundInactive)
  596. self.foreground(foregroundInactive)
  597. local resetTimeout
  598. self.onclick = function(x, y)
  599. if resetTimeout ~= nil then
  600. self.prog().cancelTimeout(resetTimeout)
  601. end
  602. self.background(backgroundActive)
  603. self.foreground(foregroundActive)
  604. self.emit("mustredraw", self)
  605. self.emit("focus", self)
  606. self.emit("click")
  607. resetTimeout = self.prog().setTimeout(0.3, function()
  608. self.background(backgroundInactive)
  609. self.foreground(foregroundInactive)
  610. self.emit("mustredraw", self)
  611. end)
  612. end
  613. return self
  614. end
  615. function DummyScreen()
  616. local self = {}
  617. self.getSize = function()
  618. return term.getSize()
  619. end
  620. self.clear = function() end
  621. self.write = function() end
  622. self.setCursorPos = function() end
  623. self.setTextColor = function() end
  624. self.setBackgroundColor = function() end
  625. return self
  626. end
  627. function Gui(prog, screen)
  628. local self = util.newObject()
  629. if screen == nil then
  630. screen = term
  631. end
  632. local focusedWidget = nil
  633. local rootWidget = nil
  634. self.getset("width",
  635. function()
  636. local x, y = screen.getSize()
  637. return x
  638. end)
  639. self.getset("height",
  640. function()
  641. local x, y = screen.getSize();
  642. return y
  643. end)
  644. self.getset("screen",
  645. function() return screen end,
  646. function(val) screen = val end)
  647. self.getset("focused",
  648. function() return focusedWidget end)
  649. -- Handle focusing and unfocusing of widgets
  650. local function onRootChildFocus(widget)
  651. if focusedWidget ~= nil then
  652. focusedWidget.unfocus()
  653. end
  654. focusedWidget = widget
  655. end
  656. local function onRootChildUnfocus(widget)
  657. if widget == focusedWidget then
  658. focusedWidget = nil
  659. end
  660. end
  661. local function onRootChildRedraw(widget)
  662. widget.draw(screen)
  663. end
  664. self.setRoot = function(root)
  665. if rootWidget ~= nil then
  666. rootWidget.removeListener("focus", onRootChildFocus)
  667. rootWidget.removeListener("unfocus", onRootChildUnfocus)
  668. rootWidget.removeListener("mustreflow", self.reflow)
  669. rootWidget.removeListener("mustredraw", onRootChildRedraw)
  670. end
  671. rootWidget = root
  672. root._parent(self)
  673. root.on("focus", onRootChildFocus)
  674. root.on("unfocus", onRootChildUnfocus)
  675. root.on("mustreflow", self.reflow)
  676. root.on("mustredraw", onRootChildRedraw)
  677. root.prog(prog)
  678. end
  679. self.reflow = function()
  680. if rootWidget == nil then
  681. return
  682. end
  683. screen.clear()
  684. screen.setCursorPos(1, 1)
  685. rootWidget.reflow(screen, 1, 1)
  686. rootWidget.draw(screen)
  687. end
  688. self.exit = function()
  689. prog.exit()
  690. end
  691. prog.on("init", self.draw)
  692. prog.on("exit", function()
  693. screen.setTextColor(colors.white)
  694. screen.setBackgroundColor(colors.black)
  695. screen.clear()
  696. screen.setCursorPos(1, 1)
  697. end)
  698. prog.on("mouse_click", function(button, x, y)
  699. if focusedWidget ~= nil then
  700. focusedWidget.unfocus()
  701. end
  702. if rootWidget ~= nil then
  703. rootWidget.onclick(x, y)
  704. end
  705. end)
  706. prog.on("char", function(char)
  707. if focusedWidget ~= nil then
  708. focusedWidget.onchar(char)
  709. end
  710. end)
  711. prog.on("key", function(key)
  712. if focusedWidget ~= nil then
  713. focusedWidget.onkey(key)
  714. end
  715. end)
  716. return self
  717. end