@teaui/core
Version:
A high-level terminal UI library for Node
296 lines • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Viewport = void 0;
const sys_1 = require("./sys");
const ansi_1 = require("./ansi");
const Style_1 = require("./Style");
const geometry_1 = require("./geometry");
const util_1 = require("./util");
/**
* Defines a region (contentSize) in which to draw, and a subset (visibleRect) that
* is on-screen. Anything not in the visibleRect is considered invisible (and any
* drawing outside the rect will be clipped)
*/
class Viewport {
#terminal;
#currentRender = null;
#contentSize;
#visibleRect;
#offset;
#screen;
#style;
/**
* For modals, this is the Rect of the view that presented the modal, in absolute
* coordinates
*/
parentRect;
constructor(screen, terminal, contentSize) {
const rect = new geometry_1.Rect(geometry_1.Point.zero, contentSize);
this.#terminal = terminal;
this.#screen = screen;
this.#contentSize = contentSize;
this.parentRect = rect;
this.#visibleRect = rect;
this.#offset = geometry_1.Point.zero;
this.#style = Style_1.Style.NONE;
// control visibility of props for inspect(viewport)
(0, util_1.define)(this, 'contentSize', { enumerable: true });
(0, util_1.define)(this, 'contentRect', { enumerable: true });
(0, util_1.define)(this, 'visibleRect', { enumerable: true });
}
/**
* during render, `contentSize` is what you should use for laying out your
* rectangles. in most cases this is synonymous with "visible" area, but not
* always.
*/
get contentSize() {
return this.#contentSize;
}
/*
* `visibleRect` can be used to optimize drawing. `visibleRect.origin`
* represents the first visible point, taking clipping into account.
*/
get visibleRect() {
return this.#visibleRect;
}
/*
* `contentRect` is a convenience property, useful for creating clipped inner
* regions. origin is always [0, 0] and size is contentSize.
*/
get contentRect() {
return new geometry_1.Rect(geometry_1.Point.zero, this.#contentSize);
}
get isEmpty() {
return this.#contentSize.isEmpty;
}
/**
* @return boolean Whether the modal creation was successful
*/
requestModal(modal, onClose) {
if (!this.#currentRender) {
return false;
}
return this.#screen.requestModal(this.#currentRender, modal, onClose, new geometry_1.Rect(this.#offset, this.#contentSize));
}
registerHotKey(key) {
if (!this.#currentRender) {
return;
}
this.#screen.registerHotKey(this.#currentRender, key);
}
/**
* @return boolean Whether the current render target is the focus view
*/
registerFocus() {
if (!this.#currentRender) {
return false;
}
return this.#screen.registerFocus(this.#currentRender);
}
/**
* @see MouseManager.registerMouse
*/
registerMouse(eventNames, rect) {
if (!this.#currentRender || this.#currentRender.screen !== this.#screen) {
return;
}
if (rect) {
rect = this.#visibleRect.intersection(rect);
}
else {
rect = this.#visibleRect;
}
const maxX = rect.maxX();
const maxY = rect.maxY();
const events = typeof eventNames === 'string' ? [eventNames] : eventNames;
for (let y = rect.minY(); y < maxY; ++y)
for (let x = rect.minX(); x < maxX; ++x) {
this.#screen.registerMouse(this.#currentRender, this.#offset, new geometry_1.Point(x, y), events);
}
}
registerTick() {
if (!this.#currentRender) {
return;
}
this.#screen.registerTick(this.#currentRender);
}
/**
* Clears out, and optionally "paints" default foreground/background colors. If no
* region is provided, the entire visibleRect is painted.
*/
paint(defaultStyle, region) {
if (region instanceof geometry_1.Point) {
this.write(ansi_1.BG_DRAW, region, defaultStyle);
}
else {
region ??= this.visibleRect;
region.forEachPoint(pt => this.write(ansi_1.BG_DRAW, pt, defaultStyle));
}
}
/**
* Does not support newlines (no default wrapping behavior),
* always prints left-to-right.
*/
write(input, to, style) {
const minX = this.#visibleRect.minX(), maxX = this.#visibleRect.maxX(), minY = this.#visibleRect.minY(), maxY = this.#visibleRect.maxY();
if (to.x >= maxX || to.y < minY || to.y >= maxY) {
return;
}
style ??= this.#style;
const startingStyle = style;
let x = to.x, y = to.y;
for (const char of sys_1.unicode.printableChars(input)) {
if (char === '\n') {
x = to.x;
y += 1;
if (y >= maxY) {
break;
}
continue;
}
if (x >= maxX) {
continue;
}
const width = sys_1.unicode.charWidth(char);
if (width === 0) {
style =
char === ansi_1.RESET
? startingStyle
: startingStyle.merge(Style_1.Style.fromSGR(char, startingStyle));
}
else if (x >= minX && x + width - 1 < maxX) {
this.#terminal.writeChar(char, this.#offset.x + x, this.#offset.y + y, style);
if (this.#currentRender &&
// if the currentRender wasn't added as a child to the screen's tree,
// we shouldn't perform this check
this.#currentRender.screen === this.#screen) {
this.#screen.checkMouse(this.#currentRender, this.#offset.x + x, this.#offset.y + y);
}
}
x += width;
}
}
/**
* Forwards 'meta' ANSI sequences (see ITerm) to the terminal
*/
writeMeta(str) {
this.#terminal.writeMeta(str);
}
usingPen(...args) {
const prevStyle = this.#style;
const pen = new Pen(prevStyle, (style) => {
this.#style = style ?? prevStyle;
});
if (args.length === 2) {
if (args[0] && args[0] !== Style_1.Style.NONE) {
pen.replacePen(args[0]);
}
args[1](pen);
}
else {
args[0](pen);
}
this.#style = prevStyle;
}
_render(view, clip, draw) {
const prevRender = this.#currentRender;
this.#currentRender = view;
this.clipped(clip, draw);
this.#currentRender = prevRender;
}
clipped(...args) {
let clip;
let style;
let draw;
if (args.length === 3) {
;
[clip, style, draw] = args;
}
else {
;
[clip, draw] = args;
style = this.#style;
}
const offsetX = this.#offset.x + clip.origin.x;
const offsetY = this.#offset.y + clip.origin.y;
const contentWidth = Math.max(0, clip.size.width);
const contentHeight = Math.max(0, clip.size.height);
// visibleRect.origin doesn't go negative - Math.max(0) prevents that.
// The subtraction of clip.origin.x only has an effect when the clipped
// origin is *negative*. In that case, the effect is that (0, 0) is outside the
// visible space, and so visibleRect.origin represents the first visiblePoint
// (in local coordinates). Basically - trust this math, it looks wrong, but I
// double checked it.
const visibleMinX = Math.max(0, this.#visibleRect.origin.x - clip.origin.x);
const visibleMinY = Math.max(0, this.#visibleRect.origin.y - clip.origin.y);
const visibleMaxX = Math.min(clip.size.width, this.#visibleRect.origin.x + this.#visibleRect.size.width - clip.origin.x);
const visibleMaxY = Math.min(clip.size.height, this.#visibleRect.origin.y +
this.#visibleRect.size.height -
clip.origin.y);
const contentSize = new geometry_1.Size(contentWidth, contentHeight);
const visibleRect = new geometry_1.Rect(new geometry_1.Point(visibleMinX, visibleMinY), new geometry_1.Size(visibleMaxX - visibleMinX, visibleMaxY - visibleMinY));
const offset = new geometry_1.Point(offsetX, offsetY);
const prevContentSize = this.#contentSize;
const prevVisibleRect = this.#visibleRect;
const prevOffset = this.#offset;
const prevStyle = this.#style;
this.#contentSize = contentSize;
this.#visibleRect = visibleRect;
this.#offset = offset;
this.#style = style;
draw(this);
this.#contentSize = prevContentSize;
this.#visibleRect = prevVisibleRect;
this.#offset = prevOffset;
this.#style = prevStyle;
}
}
exports.Viewport = Viewport;
class Pen {
#setter;
#initial;
#stack;
constructor(initialStyle, setter) {
this.#setter = setter;
this.#initial = initialStyle;
this.#stack = [];
}
/**
* Used in Text drawing components - the component usually defines a starting
* style (`viewport.usingPen(style, pen => {})`), and as it prints characters
* (`char of unicode.printableChars(line)`) it will detect 0-width SGR codes
* (`unicode.charWidth(char) === 0`). These codes can be used to create a `Style`
* object (`Style.fromSGR(char)`).
*
* SGR codes do support turn-on/turn-off, but this doesn't work well when, say, the
* default style already has certain features turned on. For instance, if the
* string specifies one region to be bold, but the entire Text component is meant
* to be bold, the behaviour of "turn-off-bold" is actually incorrect here.
*
* This is why the `fromSGR` method accepts the default style - it can be compared
* with the SGR state to determine what to do.
*/
mergePen(style) {
const current = this.#stack[0] ?? this.#initial;
style = current.merge(style);
this.replacePen(style);
}
/**
* replacePen is better when you need to control the drawing style, but you will
* assign the entire desired style.
*/
replacePen(style) {
this.#stack[0] = style;
this.#setter(style);
}
pushPen(style = undefined) {
style ??= this.#stack[0] ?? this.#initial;
// yeah I know I said pushPen but #style[0] is easier!
this.#stack.unshift(style);
this.#setter(style);
}
popPen() {
this.#setter(this.#stack.shift());
}
}
//# sourceMappingURL=Viewport.js.map