UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

374 lines 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.View = void 0; exports.parseFlexShorthand = parseFlexShorthand; const Theme_1 = require("./Theme"); const events_1 = require("./events"); const geometry_1 = require("./geometry"); function parseFlexShorthand(flex) { if (flex === 'natural') { return 'natural'; } else if (typeof flex === 'string') { return +flex.slice('flex'.length); // 'flexN' } return flex; } class View { // id = performance.now().toString(36) parent = undefined; debug = false; #screen = undefined; #theme; #prevSizeCache = new Map(); #viewportContentSize = geometry_1.Size.zero; #renderedContentSize = geometry_1.Size.zero; #invalidateParent = true; #x; #y; #width; #height; #minWidth; #minHeight; #maxWidth; #maxHeight; #isVisible = true; padding; flex = 'natural'; // mouse handling helpers #isHover = false; #isPressed = false; constructor(props = {}) { this.#update(props); const render = this.render.bind(this); const naturalSize = this.naturalSize.bind(this); Object.defineProperties(this, { render: { enumerable: false, value: this.#renderWrap(render).bind(this), }, naturalSize: { enumerable: false, value: this.#naturalSizeWrap(naturalSize).bind(this), }, // don't want to include these in inspect output parent: { enumerable: false, }, debug: { enumerable: false, }, }); } update(props) { this.#update(props); this.invalidateSize(); } #update({ theme, x, y, width, height, minWidth, minHeight, maxWidth, maxHeight, isVisible, padding, flex, debug, }) { this.#theme = typeof theme === 'string' ? Theme_1.Theme[theme] : theme; this.#x = x; this.#y = y; this.#width = width; this.#height = height; this.#minWidth = minWidth; this.#minHeight = minHeight; this.#maxWidth = maxWidth; this.#maxHeight = maxHeight; this.#isVisible = isVisible ?? true; this.padding = toEdges(padding); this.flex = flex === undefined ? 'natural' : parseFlexShorthand(flex); this.debug = debug ?? false; Object.defineProperties(this, { // only include these if they were defined padding: { enumerable: padding !== undefined, }, flex: { enumerable: flex !== undefined, }, }); } get theme() { return this.#theme ?? this.parent?.childTheme(this) ?? Theme_1.Theme.plain; } set theme(value) { this.#theme = value; } childTheme(_view) { return this.theme; } get isVisible() { return this.#isVisible; } set isVisible(value) { this.#isVisible = value; this.invalidateSize(); } get screen() { return this.#screen; } get children() { return []; } get contentSize() { return this.#renderedContentSize; } get isHover() { return this.#isHover; } get isPressed() { return this.#isPressed; } get width() { return this.#width; } get height() { return this.#height; } /** * Called from a view when a property change could affect naturalSize */ invalidateSize() { this.#prevSizeCache = new Map(); if (this.#invalidateParent) { this.parent?.invalidateSize(); } this.invalidateRender(); } /** * Indicates that a rerender is needed (but size is not affected) */ invalidateRender() { this.#screen?.needsRender(); } #toDimension(dim, available, natural, prefer) { if (dim === 'fill') { return available; } else if (dim === 'shrink') { return prefer === 'shrink' ? 0 : available; } else if (dim === 'natural') { return natural(); } return dim; } #restrictSize(_calcSize, available, prefer) { let memo; const calcSize = () => { return (memo ??= _calcSize()); }; if (this.#width !== undefined && this.#height !== undefined) { // shortcut for explicit or 'fill' on both width & height, skip all the rest const width = this.#toDimension(this.#width, available.width, () => calcSize().width, prefer), height = this.#toDimension(this.#height, available.height, () => calcSize().height, prefer); return new geometry_1.Size(width, height).mutableCopy(); } const size = (prefer === 'shrink' ? calcSize() : available).mutableCopy(); if (this.#width !== undefined) { size.width = this.#toDimension(this.#width, available.width, () => calcSize().width, prefer); } else { if (this.#minWidth !== undefined) { size.width = Math.max(this.#minWidth, size.width); } if (this.#maxWidth !== undefined) { size.width = Math.min(this.#maxWidth, size.width); } } if (this.#height !== undefined) { size.height = this.#toDimension(this.#height, available.height, () => calcSize().height, prefer); } else { if (this.#minHeight !== undefined) { size.height = Math.max(this.#minHeight, size.height); } if (this.#maxHeight !== undefined) { size.height = Math.min(this.#maxHeight, size.height); } } return size; } #calculateAvailableSize(parentAvailableSize) { const available = parentAvailableSize.mutableCopy(); if (this.#x || this.#y) { available.width -= this.#x ?? 0; available.height -= this.#y ?? 0; } if (typeof this.#width === 'number') { available.width = this.#width; } else { if (this.#maxWidth !== undefined) { available.width = Math.min(this.#maxWidth, available.width); } if (this.#minWidth !== undefined) { available.width = Math.max(this.#minWidth, available.width); } } if (typeof this.#height === 'number') { available.height = this.#height; } else { if (this.#maxHeight !== undefined) { available.height = Math.min(this.#maxHeight, available.height); } if (this.#minHeight !== undefined) { available.height = Math.max(this.#minHeight, available.height); } } if (this.padding) { available.width -= this.padding.left + this.padding.right; available.height -= this.padding.top + this.padding.bottom; } available.width = Math.max(0, available.width); available.height = Math.max(0, available.height); return available; } #naturalSizeWrap(naturalSize) { return parentAvailableSize => { const cached = this.#prevSizeCache.get(cacheKey(parentAvailableSize)); if (cached) { return cached; } const available = this.#calculateAvailableSize(parentAvailableSize); const size = this.#restrictSize(() => { let size = naturalSize(available); if (this.padding) { size = size.grow(this.padding.left + this.padding.right, this.padding.top + this.padding.bottom); } return size; }, available, 'shrink'); if (this.#x) { size.width += this.#x; } if (this.#y) { size.height += this.#y; } this.#prevSizeCache.set(cacheKey(available), size); return size; }; } #renderWrap(render) { return viewport => { if (this.#viewportContentSize.width !== viewport.contentSize.width || this.#viewportContentSize.height !== viewport.contentSize.height) { this.#invalidateParent = false; this.invalidateSize(); this.#invalidateParent = true; } this.#viewportContentSize = viewport.contentSize; let origin; const contentSize = viewport.contentSize.mutableCopy(); if (this.#x || this.#y) { origin = new geometry_1.Point(this.#x ?? 0, this.#y ?? 0); contentSize.width -= origin.x; contentSize.height -= origin.y; } else { origin = geometry_1.Point.zero; } if (this.padding) { origin = origin.offset(this.padding.left, this.padding.top); contentSize.width -= this.padding.left + this.padding.right; contentSize.height -= this.padding.top + this.padding.bottom; } this.#renderedContentSize = this.#restrictSize(() => this.naturalSize(contentSize), contentSize, 'grow'); const rect = new geometry_1.Rect(origin, this.#renderedContentSize); viewport._render(this, rect, render); }; } /** * Called before being added to the parent View */ willMoveTo(parent) { } /** * Called after being removed from the parent View */ didMoveFrom(parent) { } /** * Called after being added to a Screen */ didMount(screen) { } /** * Called after being removed from a Screen (even when about to be moved to a new * screen). */ didUnmount(screen) { } removeFromParent() { if (!this.parent) { return; } this.parent.removeChild(this); } moveToScreen(screen) { if (this.#screen === screen) { return; } const prev = this.#screen; this.#screen = screen; if (screen) { if (prev) { this.didUnmount(prev); } this.didMount(screen); } else { this.didUnmount(prev); } } /** * To register for this event, call `viewport.registerFocus()`, which returns `true` * if the current view has the keyboard focus. */ receiveKey(event) { } /** * To register for this event, call `viewport.registerMouse()` */ receiveMouse(event, system) { if ((0, events_1.isMousePressStart)(event)) { this.#isPressed = true; } else if ((0, events_1.isMousePressExit)(event)) { this.#isPressed = false; } if ((0, events_1.isMouseEnter)(event)) { this.#isHover = true; } else if ((0, events_1.isMouseExit)(event)) { this.#isHover = false; } } /** * Receives the time-delta between previous and current render. Return 'true' if * this function causes the view to need a rerender. * * To register for this event, call `viewport.registerTick()` */ receiveTick(dt) { return false; } } exports.View = View; function toEdges(edges) { if (!edges) { return; } if (typeof edges === 'number') { return { top: edges, right: edges, bottom: edges, left: edges, }; } return { top: edges.top ?? 0, right: edges.right ?? 0, bottom: edges.bottom ?? 0, left: edges.left ?? 0, }; } function cacheKey(size) { return `${size.width}x${size.height}`; } //# sourceMappingURL=View.js.map