diff options
Diffstat (limited to 'ui/form/ScrollBar.js')
-rw-r--r-- | ui/form/ScrollBar.js | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/ui/form/ScrollBar.js b/ui/form/ScrollBar.js new file mode 100644 index 0000000..13ba7fe --- /dev/null +++ b/ui/form/ScrollBar.js @@ -0,0 +1,121 @@ +const DisplayElement = require('../DisplayElement') + +const ansi = require('../../util/ansi') +const unic = require('../../util/unichars') + +module.exports = class ScrollBar extends DisplayElement { + constructor({ + getLayoutType, + getCurrentScroll, + getMaximumScroll, + getTotalItems + }) { + super() + + this.getLayoutType = getLayoutType + this.getCurrentScroll = getCurrentScroll + this.getMaximumScroll = getMaximumScroll + this.getTotalItems = getTotalItems + } + + fixLayout() { + // Normally we'd subtract one from contentW/contentH when setting the x/y + // position, but the scroll-bar is actually displayed OUTSIDE of (adjacent + // to) the parent's content area. + if (this.getLayoutType() === 'vertical') { + this.h = this.parent.contentH + this.w = 1 + this.x = this.parent.contentW + this.y = 0 + } else { + this.h = 1 + this.w = this.parent.contentW + this.x = 0 + this.y = this.parent.contentH + } + } + + drawTo(writable) { + // Uuuurgh + this.fixLayout() + + // TODO: Horizontal layout! Not functionally a lot different, but I'm too + // lazy to write a test UI for it right now. + + const { + backwards: canScrollBackwards, + forwards: canScrollForwards + } = this.getScrollableDirections() + + // - 2 for extra UI elements (arrows) + const totalLength = this.h - 2 + + // ..[-----].. + // ^start| + // ^end + // + // Start and end should correspond to how much of the scroll area + // is currently visible. So, if you can see 60% of the full scroll length + // at a time, and you are scrolled 10% down, the start position of the + // handle should be 10% down, and it should extend 60% of the scrollbar + // length, to the 70% mark. + + // NB: I think this math mixes the units for "items" and "lines". + // edgeLength is measured in lines, while totalItems is a number of items. + // This isn't a problem when the length of an item is equal to one line, + // but it's still worth investigating at some point. + const currentScroll = this.getCurrentScroll() + const totalItems = this.getTotalItems() + const edgeLength = this.parent.contentH + const visibleAtOnce = Math.min(totalItems, edgeLength) + const handleLength = visibleAtOnce / totalItems * totalLength + let handlePosition = Math.floor(totalLength / totalItems * currentScroll) + + // Silly peeve of mine: The handle should only be visibly touching the top + // or bottom of the scrollbar area if you're actually scrolled all the way + // to the start or end. Otherwise, it shouldn't be touching! There should + // visible space indicating that you can scroll in that direction + // (in addition to the arrows we show at the ends). + + if (canScrollBackwards && handlePosition === 0) { + handlePosition = 1 + } + + if (canScrollForwards && (handlePosition + handleLength) === edgeLength) { + handlePosition-- + } + + if (this.getLayoutType() === 'vertical') { + const start = this.absTop + handlePosition + 1 + for (let i = 0; i < handleLength; i++) { + writable.write(ansi.moveCursor(start + i, this.absLeft)) + writable.write(unic.BOX_V_DOUBLE) + } + + if (canScrollBackwards) { + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + writable.write(unic.ARROW_UP_DOUBLE) + } + + if (canScrollForwards) { + writable.write(ansi.moveCursor(this.absBottom, this.absLeft)) + writable.write(unic.ARROW_DOWN_DOUBLE) + } + } + } + + getScrollableDirections() { + const currentScroll = this.getCurrentScroll() + const maximumScroll = this.getMaximumScroll() + + return { + backwards: (currentScroll > 0), + forwards: (currentScroll < maximumScroll) + } + } + + canScrollAtAll() { + const {backwards, forwards} = this.getScrollableDirections() + return backwards || forwards + } +} |