123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- import * as ansi from 'tui-lib/util/ansi'
- import telc from 'tui-lib/util/telchars'
- import DisplayElement from './DisplayElement.js'
- export default class Root extends DisplayElement {
- // An element to be used as the root of a UI. Handles lots of UI and
- // socket stuff.
- constructor(interfaceArg, writable = null) {
- super()
- this.interface = interfaceArg
- this.writable = writable || interfaceArg
- this.selectedElement = null
- this.cursorBlinkOffset = Date.now()
- this.oldSelectionStates = []
- this.interface.on('inputData', buf => this.handleData(buf))
- this.renderCount = 0
- }
- handleData(buffer) {
- if (telc.isMouse(buffer)) {
- const allData = telc.parseMouse(buffer)
- const { button, line, col } = allData
- const topEl = this.getElementAt(col - 1, line - 1)
- if (topEl) {
- //console.log('Clicked', topEl.constructor.name, 'of', topEl.parent.constructor.name)
- this.eachAncestor(topEl, el => {
- if (typeof el.clicked === 'function') {
- return el.clicked(button, allData) === false
- }
- })
- }
- } else {
- this.eachAncestor(this.selectedElement, el => {
- if (typeof el.keyPressed === 'function') {
- const shouldBreak = (el.keyPressed(buffer) === false)
- if (shouldBreak) {
- return true
- }
- el.emit('keypressed', buffer)
- }
- })
- }
- }
- eachAncestor(topEl, func) {
- // Handy function for doing something to an element and all its ancestors,
- // allowing for the passed function to return false to break the loop and
- // stop propagation.
- if (topEl) {
- const els = [topEl, ...topEl.directAncestors]
- for (const el of els) {
- const shouldBreak = func(el)
- if (shouldBreak) {
- break
- }
- }
- }
- }
- drawTo(writable) {
- writable.write(ansi.moveCursor(0, 0))
- writable.write(' '.repeat(this.w * this.h))
- }
- scheduleRender() {
- if (!this.scheduledRender) {
- setTimeout(() => {
- this.scheduledRender = false
- this.render()
- })
- this.scheduledRender = true
- }
- }
- render() {
- this.renderTo(this.writable)
- }
- renderNow() {
- this.renderNowTo(this.writable)
- }
- renderTo(writable) {
- if (this.anyDescendantShouldRender()) {
- this.renderNowTo(writable)
- }
- }
- renderNowTo(writable) {
- if (writable) {
- this.renderCount++
- super.renderTo(writable)
- // Since shouldRender is false, super.renderTo won't call didRenderTo for
- // us. We need to do that ourselves.
- this.didRenderTo(writable)
- }
- }
- anyDescendantShouldRender() {
- let render = false
- this.eachDescendant(el => {
- // If we already know we're going to render, checking the element's
- // scheduled-draw status (which involves iterating over each of its draw
- // dependency properties) is redundant.
- if (render) {
- return
- }
- render = el.hasScheduledDraw()
- })
- return render
- }
- shouldRender() {
- // We need to return false here because otherwise all children will render,
- // since they'll see the root as an ancestor who needs to be rendered. Bad!
- return false
- }
- didRenderTo(writable) {
- this.eachDescendant(el => {
- el.unscheduleDraw()
- el.updateLastDrawValues()
- })
- /*
- writable.write(ansi.moveCursorRaw(1, 1))
- writable.write('Renders: ' + this.renderCount)
- */
- // Render the cursor, based on the cursorX and cursorY of the currently
- // selected element.
- if (this.selectedElement && this.selectedElement.cursorVisible) {
- /*
- if ((Date.now() - this.cursorBlinkOffset) % 1000 < 500) {
- writable.write(ansi.moveCursor(
- this.selectedElement.absCursorY, this.selectedElement.absCursorX))
- writable.write(ansi.invert())
- writable.write('I')
- writable.write(ansi.resetAttributes())
- }
- */
- writable.write(ansi.showCursor())
- writable.write(ansi.moveCursorRaw(
- this.selectedElement.absCursorY, this.selectedElement.absCursorX))
- } else {
- writable.write(ansi.hideCursor())
- }
- this.emit('rendered')
- }
- cursorMoved() {
- // Resets the blinking animation for the cursor. Call this whenever you
- // move the cursor.
- this.cursorBlinkOffset = Date.now()
- }
- select(el, {fromForm = false} = {}) {
- // Select an element. Calls the unfocus method on the already-selected
- // element, if there is one.
- // If the element is part of a form, just be lazy and pass control to that
- // form...unless the form itself asked us to select the element!
- //
- // TODO: This is so that if an element is selected, its parent form will
- // automatically see that and correctly update its curIndex... but what if
- // the element is an input of a form which is NOT its parent?
- //
- // XXX: We currently use a HUGE HACK instead of `instanceof` to avoid
- // breaking the rule of import direction (controls -> primitives, never
- // the other way around). This is bad for obvious reasons, but I haven't
- // yet looked into what the correct approach would be.
- const parent = el.parent
- if (!fromForm && parent.constructor.name === 'Form' && parent.inputs.includes(el)) {
- parent.selectInput(el)
- return
- }
- const oldSelected = this.selectedElement
- const newSelected = el
- // Relevant elements that COULD have their "isSelected" state change.
- const relevantElements = ([
- ...(oldSelected ? [...oldSelected.directAncestors, oldSelected] : []),
- ...(newSelected ? newSelected.directAncestors : [])
- ]
- // We ignore elements where isSelected is undefined, because they aren't
- // built to handle being selected, and they break the compare-old-and-new-
- // state code below.
- .filter(el => typeof el.isSelected !== 'undefined')
- // Get rid of duplicates - including any that occurred in the already
- // existing array of selection states. (We only care about the oldest
- // selection state, i.e. the one when we did the first .select().)
- .reduce((acc, el) => {
- // Duplicates from relevant elements of current .select()
- if (acc.includes(el)) return acc
- // Duplicates from already existing selection states
- if (this.oldSelectionStates.some(x => x[0] === el)) return acc
- return acc.concat([el])
- }, []))
- // Keep track of whether those elements were selected before we call the
- // newly selected element's selected() function. We store these on a
- // property because we might actually be adding to it from a previous
- // root.select() call, if that one itself caused this root.select().
- // One all root.select()s in the "chain" (as it is) have finished, we'll
- // go through these states and call the appropriate .select/unselect()
- // functions on each element whose .isSelected changed.
- const selectionStates = relevantElements.map(el => [el, el.isSelected])
- this.oldSelectionStates = this.oldSelectionStates.concat(selectionStates)
- this.selectedElement = el
- // Same stuff as in the for loop below. We always call selected() on the
- // passed element, even if it was already selected before.
- if (el.selected) el.selected()
- if (typeof el.focused === 'function') el.focused()
- // If the selection changed as a result of the element's selected()
- // function, stop here. We will leave calling the appropriate functions on
- // the elements in the oldSelectionStates array to the final .select(),
- // i.e. the one which caused no change in selected element.
- if (this.selectedElement !== newSelected) return
- // Compare the old "isSelected" state of every relevant element with their
- // current "isSelected" state, and call the respective selected/unselected
- // functions. (Also call focused and unfocused for some sense of trying to
- // not break old programs, but, like, old programs are going to be broken
- // anyways.)
- const states = this.oldSelectionStates.slice()
- for (const [ el, wasSelected ] of states) {
- // Now that we'll have processed it, we don't want it in the array
- // anymore.
- this.oldSelectionStates.shift()
- const { isSelected } = el
- if (isSelected && !wasSelected) {
- // Don't call these functions if this element is the newly selected
- // one, because we already called them above!
- if (el !== newSelected) {
- if (el.selected) el.selected()
- if (typeof el.focused === 'function') el.focused()
- }
- } else if (wasSelected && !isSelected) {
- if (el.unselected) el.unselected()
- if (typeof el.unfocused === 'function') el.unfocused()
- }
- // If the (un)selected() handler actually selected a different element
- // itself, then further processing of new selected states is irrelevant,
- // so stop here. (We return instead of breaking the for loop because
- // anything after this loop would have already been handled by the call
- // to Root.select() from the (un)selected() handler.)
- if (this.selectedElement !== newSelected) {
- return
- }
- }
- this.cursorMoved()
- }
- isChildOrSelfSelected(el) {
- if (!this.selectedElement) return false
- if (this.selectedElement === el) return true
- if (this.selectedElement.directAncestors.includes(el)) return true
- return false
- }
- get selectedElement() { return this.getDep('selectedElement') }
- set selectedElement(v) { return this.setDep('selectedElement', v) }
- }
|