From 6ea74c268a12325296a1d2e7fc31b02030ddb8bc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 12 May 2023 17:42:09 -0300 Subject: use ESM module syntax & minor cleanups The biggest change here is moving various element classes under more scope-specific directories, which helps to avoid circular dependencies and is just cleaner to navigate and expand in the future. Otherwise this is a largely uncritical port to ESM module syntax! There are probably a number of changes and other cleanups that remain much needed. Whenever I make changes to tui-lib it's hard to believe it's already been years since the previous time. First commits are from January 2017, and the code originates a month earlier in KAaRMNoD! --- ui/primitives/DisplayElement.js | 305 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 ui/primitives/DisplayElement.js (limited to 'ui/primitives/DisplayElement.js') diff --git a/ui/primitives/DisplayElement.js b/ui/primitives/DisplayElement.js new file mode 100644 index 0000000..d2a0956 --- /dev/null +++ b/ui/primitives/DisplayElement.js @@ -0,0 +1,305 @@ +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 || el.clickThrough) { + continue + } + + const el2 = el.getElementAt(x, y) + if (el2) { + return el2 + } + + 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 } +} -- cgit 1.3.0-6-gf8a5