« get me outta code hell

tui-lib - Pure Node.js library for making visual command-line programs (ala vim, ncdu)
about summary refs log tree commit diff
path: root/ui/form/ScrollBar.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/form/ScrollBar.js')
-rw-r--r--ui/form/ScrollBar.js121
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
+  }
+}