@teaui/core
Version:
A high-level terminal UI library for Node
226 lines • 10.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Scrollable = void 0;
const Container_1 = require("../Container");
const geometry_1 = require("../geometry");
const events_1 = require("../events");
const Style_1 = require("../Style");
/**
* Scrollable is meant to scroll _a single view_, ie a Stack view. But all the
* container views are optimized to check their _visibleRect_, and won't render
* children that are not in view, saving some CPU cycles.
*/
class Scrollable extends Container_1.Container {
#showScrollbars = true;
#scrollHeight = 1;
#scrollWidth = 2;
#contentOffset;
#contentSize = geometry_1.Size.zero;
#visibleSize = geometry_1.Size.zero;
#prevMouseDown = undefined;
constructor(props) {
super(props);
this.#contentOffset = { x: 0, y: 0 };
this.#update(props);
}
update(props) {
this.#update(props);
super.update(props);
}
#update({ scrollHeight, scrollWidth, showScrollbars }) {
this.#showScrollbars = showScrollbars ?? true;
this.#scrollHeight = scrollHeight ?? 1;
this.#scrollWidth = scrollWidth ?? 2;
}
naturalSize(available) {
const size = super.naturalSize(available).mutableCopy();
size.width = Math.min(size.width, available.width);
size.height = Math.min(size.height, available.height);
return size;
}
#maxOffsetX() {
const tooTall = this.#contentSize.height > this.contentSize.height;
return this.#visibleSize.width - this.#contentSize.width + (tooTall ? 0 : 1);
}
#maxOffsetY() {
const tooWide = this.#contentSize.width > this.contentSize.width;
return (this.#visibleSize.height - this.#contentSize.height + (tooWide ? 0 : 1));
}
receiveMouse(event) {
if ((0, events_1.isMouseWheel)(event)) {
this.receiveWheel(event);
return;
}
if (event.name === 'mouse.button.up') {
this.#prevMouseDown = undefined;
return;
}
const tooWide = this.#contentSize.width > this.contentSize.width;
const tooTall = this.#contentSize.height > this.contentSize.height;
if (tooWide &&
tooTall &&
event.position.y === this.contentSize.height - 1 &&
event.position.x === this.contentSize.width - 1) {
// bottom-right corner click
return;
}
if (this.#prevMouseDown === undefined) {
if (tooWide && event.position.y === this.contentSize.height) {
this.#prevMouseDown = 'horizontal';
}
else if (tooTall && event.position.x === this.contentSize.width) {
this.#prevMouseDown = 'vertical';
}
else {
return;
}
}
this.receiveMouseDown(event);
}
receiveMouseDown(event) {
const tooWide = this.#contentSize.width > this.contentSize.width;
const tooTall = this.#contentSize.height > this.contentSize.height;
if (tooWide && this.#prevMouseDown === 'horizontal') {
const maxX = this.#maxOffsetX();
const offsetX = Math.round((0, geometry_1.interpolate)(event.position.x, [0, this.contentSize.width - (tooTall ? 1 : 0)], [0, maxX]));
this.#contentOffset = {
x: Math.max(maxX, Math.min(0, offsetX)),
y: this.#contentOffset.y,
};
}
else if (tooTall && this.#prevMouseDown === 'vertical') {
const maxY = this.#maxOffsetY();
const offsetY = Math.round((0, geometry_1.interpolate)(event.position.y, [0, this.contentSize.height - (tooWide ? 1 : 0)], [0, maxY]));
this.#contentOffset = {
x: this.#contentOffset.x,
y: Math.max(maxY, Math.min(0, offsetY)),
};
}
}
receiveWheel(event) {
let deltaY = 0, deltaX = 0;
if (event.name === 'mouse.wheel.up') {
deltaY = this.#scrollHeight * -1;
}
else if (event.name === 'mouse.wheel.down') {
deltaY = this.#scrollHeight;
}
else if (event.name === 'mouse.wheel.left') {
deltaX = this.#scrollWidth;
}
else if (event.name === 'mouse.wheel.right') {
deltaX = this.#scrollWidth * -1;
}
if (event.ctrl) {
deltaY *= 5;
deltaX *= 5;
}
const tooTall = (this.#contentSize?.height ?? 0) > this.contentSize.height;
if (!tooTall && deltaX === 0) {
deltaX = deltaY;
}
this.scrollBy(deltaX, deltaY);
}
/**
* Moves the visible region. The visible region is stored as a pointer to the
* top-most row and an offset from the top of that row (see `interface ContentOffset`)
*
* Positive offset scrolls *down* (currentOffset goes more negative)
*
* When current cell is entirely above the top, we set the `contentOffset` to the
* row that is at the top of the screen and still visible, similarly if the current
* cell is below the top, we fetch enough rows about and update the `contentOffset`
* to point to the top-most row.
*/
scrollBy(offsetX, offsetY) {
if (offsetX === 0 && offsetY === 0) {
return;
}
const tooWide = this.#contentSize.width > this.contentSize.width;
const tooTall = this.#contentSize.height > this.contentSize.height;
let { x, y } = this.#contentOffset;
const maxX = this.#maxOffsetX();
const maxY = this.#maxOffsetY();
x = Math.min(0, Math.max(maxX, x - offsetX));
y = Math.min(0, Math.max(maxY, y - offsetY));
this.#contentOffset = { x, y };
}
get contentSize() {
const delta = this.#showScrollbars ? 1 : 0;
return super.contentSize.shrink(delta, delta);
}
render(viewport) {
if (viewport.isEmpty) {
return super.render(viewport);
}
viewport.registerMouse('mouse.wheel');
let contentSize = geometry_1.Size.zero.mutableCopy();
for (const child of this.children) {
const childSize = child.naturalSize(viewport.contentSize);
contentSize.width = Math.max(contentSize.width, childSize.width);
contentSize.height = Math.max(contentSize.height, childSize.height);
}
this.#contentSize = contentSize;
const tooWide = contentSize.width > viewport.contentSize.width;
const tooTall = contentSize.height > viewport.contentSize.height;
// #contentOffset is _negative_ (indicates the amount to move the view away
// from the origin, which will always be up/left of 0,0)
const outside = new geometry_1.Rect([this.#contentOffset.x, this.#contentOffset.y], viewport.contentSize
.shrink(this.#contentOffset.x, this.#contentOffset.y)
.shrink(this.#showScrollbars && tooTall ? 1 : 0, this.#showScrollbars && tooWide ? 1 : 0));
viewport.clipped(outside, inside => {
for (const child of this.children) {
child.render(inside);
}
});
this.#visibleSize = viewport.visibleRect.size.shrink(tooWide ? 1 : 0, tooTall ? 1 : 0);
if (this.#showScrollbars && (tooWide || tooTall)) {
const scrollBar = new Style_1.Style({
foreground: this.theme.darkenColor,
background: this.theme.darkenColor,
});
const scrollControl = new Style_1.Style({
foreground: this.theme.highlightColor,
background: this.theme.highlightColor,
});
// scrollMaxX: x of the last column of the view
// scrollMaxY: y of the last row of the view
// scrollMaxHorizX: horizontal scroll bar is drawn from 0 to scrollMaxHorizX
// scrollMaxHorizY: vertical scroll bar is drawn from 0 to scrollMaxHorizY
const scrollMaxX = viewport.contentSize.width - 1, scrollMaxY = viewport.contentSize.height - 1, scrollMaxHorizX = scrollMaxX - (tooTall ? 1 : 0), scrollMaxVertY = scrollMaxY - (tooWide ? 1 : 0);
if (tooWide && tooTall) {
viewport.write('█', new geometry_1.Point(scrollMaxX, scrollMaxY), scrollBar);
}
if (tooWide) {
viewport.registerMouse('mouse.button.left', new geometry_1.Rect(new geometry_1.Point(0, scrollMaxY), new geometry_1.Size(scrollMaxHorizX + 1, 1)));
const contentOffsetX = -this.#contentOffset.x;
const viewX = Math.round((0, geometry_1.interpolate)(contentOffsetX, [
0,
contentSize.width -
viewport.contentSize.width +
(tooTall ? 1 : 0),
], [0, scrollMaxHorizX]));
for (let x = 0; x <= scrollMaxHorizX; x++) {
const inRange = x === viewX;
viewport.write(inRange ? '█' : ' ', new geometry_1.Point(x, scrollMaxY), inRange ? scrollControl : scrollBar);
}
}
if (tooTall) {
viewport.registerMouse('mouse.button.left', new geometry_1.Rect(new geometry_1.Point(scrollMaxX, 0), new geometry_1.Size(1, scrollMaxVertY + 1)));
const contentOffsetY = -this.#contentOffset.y;
const viewY = Math.round((0, geometry_1.interpolate)(contentOffsetY, [
0,
contentSize.height -
viewport.contentSize.height +
(tooWide ? 1 : 0),
], [0, scrollMaxVertY]));
for (let y = 0; y <= scrollMaxVertY; y++) {
const inRange = y === viewY;
viewport.write(inRange ? '█' : ' ', new geometry_1.Point(scrollMaxX, y), inRange ? scrollControl : scrollBar);
}
}
}
}
}
exports.Scrollable = Scrollable;
//# sourceMappingURL=Scrollable.js.map