Root.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import * as ansi from 'tui-lib/util/ansi'
  2. import telc from 'tui-lib/util/telchars'
  3. import DisplayElement from './DisplayElement.js'
  4. export default class Root extends DisplayElement {
  5. // An element to be used as the root of a UI. Handles lots of UI and
  6. // socket stuff.
  7. constructor(interfaceArg, writable = null) {
  8. super()
  9. this.interface = interfaceArg
  10. this.writable = writable || interfaceArg
  11. this.selectedElement = null
  12. this.cursorBlinkOffset = Date.now()
  13. this.oldSelectionStates = []
  14. this.interface.on('inputData', buf => this.handleData(buf))
  15. this.renderCount = 0
  16. }
  17. handleData(buffer) {
  18. if (telc.isMouse(buffer)) {
  19. const allData = telc.parseMouse(buffer)
  20. const { button, line, col } = allData
  21. const topEl = this.getElementAt(col - 1, line - 1)
  22. if (topEl) {
  23. //console.log('Clicked', topEl.constructor.name, 'of', topEl.parent.constructor.name)
  24. this.eachAncestor(topEl, el => {
  25. if (typeof el.clicked === 'function') {
  26. return el.clicked(button, allData) === false
  27. }
  28. })
  29. }
  30. } else {
  31. this.eachAncestor(this.selectedElement, el => {
  32. if (typeof el.keyPressed === 'function') {
  33. const shouldBreak = (el.keyPressed(buffer) === false)
  34. if (shouldBreak) {
  35. return true
  36. }
  37. el.emit('keypressed', buffer)
  38. }
  39. })
  40. }
  41. }
  42. eachAncestor(topEl, func) {
  43. // Handy function for doing something to an element and all its ancestors,
  44. // allowing for the passed function to return false to break the loop and
  45. // stop propagation.
  46. if (topEl) {
  47. const els = [topEl, ...topEl.directAncestors]
  48. for (const el of els) {
  49. const shouldBreak = func(el)
  50. if (shouldBreak) {
  51. break
  52. }
  53. }
  54. }
  55. }
  56. drawTo(writable) {
  57. writable.write(ansi.moveCursor(0, 0))
  58. writable.write(' '.repeat(this.w * this.h))
  59. }
  60. scheduleRender() {
  61. if (!this.scheduledRender) {
  62. setTimeout(() => {
  63. this.scheduledRender = false
  64. this.render()
  65. })
  66. this.scheduledRender = true
  67. }
  68. }
  69. render() {
  70. this.renderTo(this.writable)
  71. }
  72. renderNow() {
  73. this.renderNowTo(this.writable)
  74. }
  75. renderTo(writable) {
  76. if (this.anyDescendantShouldRender()) {
  77. this.renderNowTo(writable)
  78. }
  79. }
  80. renderNowTo(writable) {
  81. if (writable) {
  82. this.renderCount++
  83. super.renderTo(writable)
  84. // Since shouldRender is false, super.renderTo won't call didRenderTo for
  85. // us. We need to do that ourselves.
  86. this.didRenderTo(writable)
  87. }
  88. }
  89. anyDescendantShouldRender() {
  90. let render = false
  91. this.eachDescendant(el => {
  92. // If we already know we're going to render, checking the element's
  93. // scheduled-draw status (which involves iterating over each of its draw
  94. // dependency properties) is redundant.
  95. if (render) {
  96. return
  97. }
  98. render = el.hasScheduledDraw()
  99. })
  100. return render
  101. }
  102. shouldRender() {
  103. // We need to return false here because otherwise all children will render,
  104. // since they'll see the root as an ancestor who needs to be rendered. Bad!
  105. return false
  106. }
  107. didRenderTo(writable) {
  108. this.eachDescendant(el => {
  109. el.unscheduleDraw()
  110. el.updateLastDrawValues()
  111. })
  112. /*
  113. writable.write(ansi.moveCursorRaw(1, 1))
  114. writable.write('Renders: ' + this.renderCount)
  115. */
  116. // Render the cursor, based on the cursorX and cursorY of the currently
  117. // selected element.
  118. if (this.selectedElement && this.selectedElement.cursorVisible) {
  119. /*
  120. if ((Date.now() - this.cursorBlinkOffset) % 1000 < 500) {
  121. writable.write(ansi.moveCursor(
  122. this.selectedElement.absCursorY, this.selectedElement.absCursorX))
  123. writable.write(ansi.invert())
  124. writable.write('I')
  125. writable.write(ansi.resetAttributes())
  126. }
  127. */
  128. writable.write(ansi.showCursor())
  129. writable.write(ansi.moveCursorRaw(
  130. this.selectedElement.absCursorY, this.selectedElement.absCursorX))
  131. } else {
  132. writable.write(ansi.hideCursor())
  133. }
  134. this.emit('rendered')
  135. }
  136. cursorMoved() {
  137. // Resets the blinking animation for the cursor. Call this whenever you
  138. // move the cursor.
  139. this.cursorBlinkOffset = Date.now()
  140. }
  141. select(el, {fromForm = false} = {}) {
  142. // Select an element. Calls the unfocus method on the already-selected
  143. // element, if there is one.
  144. // If the element is part of a form, just be lazy and pass control to that
  145. // form...unless the form itself asked us to select the element!
  146. //
  147. // TODO: This is so that if an element is selected, its parent form will
  148. // automatically see that and correctly update its curIndex... but what if
  149. // the element is an input of a form which is NOT its parent?
  150. //
  151. // XXX: We currently use a HUGE HACK instead of `instanceof` to avoid
  152. // breaking the rule of import direction (controls -> primitives, never
  153. // the other way around). This is bad for obvious reasons, but I haven't
  154. // yet looked into what the correct approach would be.
  155. const parent = el.parent
  156. if (!fromForm && parent.constructor.name === 'Form' && parent.inputs.includes(el)) {
  157. parent.selectInput(el)
  158. return
  159. }
  160. const oldSelected = this.selectedElement
  161. const newSelected = el
  162. // Relevant elements that COULD have their "isSelected" state change.
  163. const relevantElements = ([
  164. ...(oldSelected ? [...oldSelected.directAncestors, oldSelected] : []),
  165. ...(newSelected ? newSelected.directAncestors : [])
  166. ]
  167. // We ignore elements where isSelected is undefined, because they aren't
  168. // built to handle being selected, and they break the compare-old-and-new-
  169. // state code below.
  170. .filter(el => typeof el.isSelected !== 'undefined')
  171. // Get rid of duplicates - including any that occurred in the already
  172. // existing array of selection states. (We only care about the oldest
  173. // selection state, i.e. the one when we did the first .select().)
  174. .reduce((acc, el) => {
  175. // Duplicates from relevant elements of current .select()
  176. if (acc.includes(el)) return acc
  177. // Duplicates from already existing selection states
  178. if (this.oldSelectionStates.some(x => x[0] === el)) return acc
  179. return acc.concat([el])
  180. }, []))
  181. // Keep track of whether those elements were selected before we call the
  182. // newly selected element's selected() function. We store these on a
  183. // property because we might actually be adding to it from a previous
  184. // root.select() call, if that one itself caused this root.select().
  185. // One all root.select()s in the "chain" (as it is) have finished, we'll
  186. // go through these states and call the appropriate .select/unselect()
  187. // functions on each element whose .isSelected changed.
  188. const selectionStates = relevantElements.map(el => [el, el.isSelected])
  189. this.oldSelectionStates = this.oldSelectionStates.concat(selectionStates)
  190. this.selectedElement = el
  191. // Same stuff as in the for loop below. We always call selected() on the
  192. // passed element, even if it was already selected before.
  193. if (el.selected) el.selected()
  194. if (typeof el.focused === 'function') el.focused()
  195. // If the selection changed as a result of the element's selected()
  196. // function, stop here. We will leave calling the appropriate functions on
  197. // the elements in the oldSelectionStates array to the final .select(),
  198. // i.e. the one which caused no change in selected element.
  199. if (this.selectedElement !== newSelected) return
  200. // Compare the old "isSelected" state of every relevant element with their
  201. // current "isSelected" state, and call the respective selected/unselected
  202. // functions. (Also call focused and unfocused for some sense of trying to
  203. // not break old programs, but, like, old programs are going to be broken
  204. // anyways.)
  205. const states = this.oldSelectionStates.slice()
  206. for (const [ el, wasSelected ] of states) {
  207. // Now that we'll have processed it, we don't want it in the array
  208. // anymore.
  209. this.oldSelectionStates.shift()
  210. const { isSelected } = el
  211. if (isSelected && !wasSelected) {
  212. // Don't call these functions if this element is the newly selected
  213. // one, because we already called them above!
  214. if (el !== newSelected) {
  215. if (el.selected) el.selected()
  216. if (typeof el.focused === 'function') el.focused()
  217. }
  218. } else if (wasSelected && !isSelected) {
  219. if (el.unselected) el.unselected()
  220. if (typeof el.unfocused === 'function') el.unfocused()
  221. }
  222. // If the (un)selected() handler actually selected a different element
  223. // itself, then further processing of new selected states is irrelevant,
  224. // so stop here. (We return instead of breaking the for loop because
  225. // anything after this loop would have already been handled by the call
  226. // to Root.select() from the (un)selected() handler.)
  227. if (this.selectedElement !== newSelected) {
  228. return
  229. }
  230. }
  231. this.cursorMoved()
  232. }
  233. isChildOrSelfSelected(el) {
  234. if (!this.selectedElement) return false
  235. if (this.selectedElement === el) return true
  236. if (this.selectedElement.directAncestors.includes(el)) return true
  237. return false
  238. }
  239. get selectedElement() { return this.getDep('selectedElement') }
  240. set selectedElement(v) { return this.setDep('selectedElement', v) }
  241. }