123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- import Element from './Element.js'
- export default class DisplayElement extends Element {
- // A general class that handles dealing with screen coordinates, the tree
- // of elements, and other common stuff.
- //
- // This element doesn't handle any real rendering; just layouts. Placing
- // characters at specific positions should be implemented in subclasses.
- //
- // It's a subclass of EventEmitter, so you can make your own events within
- // the logic of your subclass.
- static drawValues = Symbol('drawValues')
- static lastDrawValues = Symbol('lastDrawValues')
- static scheduledDraw = Symbol('scheduledDraw')
- constructor() {
- super()
- this[DisplayElement.drawValues] = {}
- this[DisplayElement.lastDrawValues] = {}
- this[DisplayElement.scheduledDraw] = false
- this.visible = true
- this.x = 0
- this.y = 0
- this.w = 0
- this.h = 0
- this.hPadding = 0
- this.vPadding = 0
- // Note! This only applies to the parent, not the children. Useful for
- // when you want an element to cover the whole screen but allow mouse
- // events to pass through.
- this.clickThrough = false
- }
- drawTo(writable) {
- // Writes text to a "writable" - an object that has a "write" method.
- // Custom rendering should be handled as an override of this method in
- // subclasses of DisplayElement.
- }
- renderTo(writable) {
- // Like drawTo, but only calls drawTo if the element is visible. Use this
- // with your root element, not drawTo.
- if (!this.visible) {
- return
- }
- const causeRenderEl = this.shouldRender()
- if (causeRenderEl) {
- this.drawTo(writable)
- this.renderChildrenTo(writable)
- this.didRenderTo(writable)
- } else {
- this.renderChildrenTo(writable)
- }
- }
- shouldRender() {
- // WIP! Until this implementation is finished, always return true (or else
- // lots of rendering breaks).
- /*
- return (
- this[DisplayElement.scheduledDraw] ||
- [...this.directAncestors].find(el => el.shouldRender())
- )
- */
- return true
- }
- renderChildrenTo(writable) {
- // Renders all of the children to a writable.
- for (const child of this.children) {
- child.renderTo(writable)
- }
- }
- didRenderTo(writable) {
- // Called immediately after rendering this element AND all of its
- // children. If you need to do something when that happens, override this
- // method in your subclass.
- //
- // It's fine to draw more things to the writable here - just keep in mind
- // that it'll be drawn over this element and its children, but not any
- // elements drawn in the future.
- }
- fixLayout() {
- // Adjusts the layout of children in this element. If your subclass has
- // any children in it, you should override this method.
- }
- fixAllLayout() {
- // Runs fixLayout on this as well as all children.
- this.fixLayout()
- for (const child of this.children) {
- child.fixAllLayout()
- }
- }
- confirmDrawValuesExists() {
- if (!this[DisplayElement.drawValues]) {
- this[DisplayElement.drawValues] = {}
- }
- }
- getDep(key) {
- this.confirmDrawValuesExists()
- return this[DisplayElement.drawValues][key]
- }
- setDep(key, value) {
- this.confirmDrawValuesExists()
- const oldValue = this[DisplayElement.drawValues][key]
- if (value !== this[DisplayElement.drawValues][key]) {
- this[DisplayElement.drawValues][key] = value
- this.scheduleDraw()
- // Grumble: technically it's possible for a root element to not be an
- // actual Root. While we don't check for this case most of the time (even
- // though we ought to), we do here because it's not unlikely for draw
- // dependency values to be changed before the element is actually added
- // to a Root element.
- if (this.root.scheduleRender) {
- this.root.scheduleRender()
- }
- }
- return value
- }
- scheduleDrawWithoutPropertyChange() {
- // Utility function for when you need to schedule a draw without updating
- // any particular draw-dependency property on the element. Works by setting
- // an otherwise unused dep to a unique object. (We can't use a symbol here,
- // because then Object.entries doesn't notice it.)
- this.setDep('drawWithoutProperty', Math.random())
- }
- scheduleDraw() {
- this[DisplayElement.scheduledDraw] = true
- }
- unscheduleDraw() {
- this[DisplayElement.scheduledDraw] = false
- }
- hasScheduledDraw() {
- if (this[DisplayElement.scheduledDraw]) {
- for (const [ key, value ] of Object.entries(this[DisplayElement.drawValues])) {
- if (value !== this[DisplayElement.lastDrawValues][key]) {
- return true
- }
- }
- }
- return false
- }
- updateLastDrawValues() {
- Object.assign(this[DisplayElement.lastDrawValues], this[DisplayElement.drawValues])
- }
- centerInParent() {
- // Utility function to center this element in its parent. Must be called
- // only when it has a parent. Set the width and height of the element
- // before centering it!
- if (this.parent === null) {
- throw new Error('Cannot center in parent when parent is null')
- }
- this.x = Math.round((this.parent.contentW - this.w) / 2)
- this.y = Math.round((this.parent.contentH - this.h) / 2)
- }
- fillParent() {
- // Utility function to fill this element in its parent. Must be called
- // only when it has a parent.
- if (this.parent === null) {
- throw new Error('Cannot fill parent when parent is null')
- }
- this.x = 0
- this.y = 0
- this.w = this.parent.contentW
- this.h = this.parent.contentH
- }
- fitToParent() {
- // Utility function to position this element so that it stays within its
- // parent's bounds. Must be called only when it has a parent.
- //
- // This function is useful when (and only when) the right or bottom edge
- // of this element may be past the right or bottom edge of its parent.
- // In such a case, the element will first be moved left or up by the
- // distance that its edge exceeds that of its parent, so that its edge is
- // no longer past the parent's. Then, if the left or top edge of the
- // element is less than zero, i.e. outside the parent, it is set to zero
- // and the element's width or height is adjusted so that it does not go
- // past the bounds of the parent.
- if (this.x + this.w > this.parent.right) {
- const offendExtent = (this.x + this.w) - this.parent.contentW
- this.x -= offendExtent
- if (this.x < 0) {
- const offstartExtent = 0 - this.x
- this.w -= offstartExtent
- this.x = 0
- }
- }
- if (this.y + this.h > this.parent.bottom) {
- const offendExtent = (this.y + this.h) - this.parent.contentH
- this.y -= offendExtent
- if (this.y < 0) {
- const offstartExtent = 0 - this.y
- this.h -= offstartExtent
- this.y = 0
- }
- }
- }
- getElementAt(x, y) {
- // Gets the topmost element at the provided absolute coordinate.
- // Note that elements which are not visible or have the clickThrough
- // property set to true are not considered.
- const children = this.children.slice()
- // Start searching the last- (top-) rendered children first.
- children.reverse()
- for (const el of children) {
- if (!el.visible) {
- continue
- }
- const el2 = el.getElementAt(x, y)
- if (el2) {
- return el2
- }
- if (el.clickThrough) {
- continue
- }
- const { absX, absY, w, h } = el
- if (absX <= x && absX + w > x) {
- if (absY <= y && absY + h > y) {
- return el
- }
- }
- }
- return null
- }
- get x() { return this.getDep('x') }
- set x(v) { return this.setDep('x', v) }
- get y() { return this.getDep('y') }
- set y(v) { return this.setDep('y', v) }
- get hPadding() { return this.getDep('hPadding') }
- set hPadding(v) { return this.setDep('hPadding', v) }
- get vPadding() { return this.getDep('vPadding') }
- set vPadding(v) { return this.setDep('vPadding', v) }
- get visible() { return this.getDep('visible') }
- set visible(v) { return this.setDep('visible', v) }
- // Commented out because this doesn't fix any problems (at least ATM).
- // get parent() { return this.getDep('parent') }
- // set parent(v) { return this.setDep('parent', v) }
- get absX() {
- if (this.parent) {
- return this.parent.contentX + this.x
- } else {
- return this.x
- }
- }
- get absY() {
- if (this.parent) {
- return this.parent.contentY + this.y
- } else {
- return this.y
- }
- }
- // Where contents should be positioned.
- get contentX() { return this.absX + this.hPadding }
- get contentY() { return this.absY + this.vPadding }
- get contentW() { return this.w - this.hPadding * 2 }
- get contentH() { return this.h - this.vPadding * 2 }
- get left() { return this.x }
- get right() { return this.x + this.w }
- get top() { return this.y }
- get bottom() { return this.y + this.h }
- get absLeft() { return this.absX }
- get absRight() { return this.absX + this.w - 1 }
- get absTop() { return this.absY }
- get absBottom() { return this.absY + this.h - 1 }
- }
|