« get me outta code hell

Make ScrollBar an independent and published class - tui-lib - Pure Node.js library for making visual command-line programs (ala vim, ncdu)
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-10-17 12:57:24 -0300
committerFlorrie <towerofnix@gmail.com>2019-10-17 12:57:24 -0300
commit742d2543b88ad4cbb2fc9a859f093a57f32c1967 (patch)
tree9db69ace2691ee3fe8880800f7049ce4c22fb70c
parentf1cc65d376141fa5e76878ffd3da4392f8a7c747 (diff)
Make ScrollBar an independent and published class
It's no longer strictly connected to a ListScrollForm, and is published,
so it's much easier to use as an element from the tui-lib API in any
project now.
-rw-r--r--index.js1
-rw-r--r--ui/form/ListScrollForm.js115
-rw-r--r--ui/form/ScrollBar.js121
3 files changed, 129 insertions, 108 deletions
diff --git a/index.js b/index.js
index de4c680..b848814 100644
--- a/index.js
+++ b/index.js
@@ -16,6 +16,7 @@ module.exports = {
       FocusElement: require('./ui/form/FocusElement'),
       Form: require('./ui/form/Form'),
       ListScrollForm: require('./ui/form/ListScrollForm'),
+      ScrollBar: require('./ui/form/ScrollBar'),
       TextInput: require('./ui/form/TextInput')
     }
   },
diff --git a/ui/form/ListScrollForm.js b/ui/form/ListScrollForm.js
index 78c376f..72e79df 100644
--- a/ui/form/ListScrollForm.js
+++ b/ui/form/ListScrollForm.js
@@ -1,9 +1,8 @@
 const ansi = require('../../util/ansi')
 const telc = require('../../util/telchars')
-const unic = require('../../util/unichars')
 
-const DisplayElement = require('../DisplayElement')
 const Form = require('./Form')
+const ScrollBar = require('./ScrollBar')
 
 module.exports = class ListScrollForm extends Form {
   // A form that lets the user scroll through a list of items. It
@@ -21,7 +20,12 @@ module.exports = class ListScrollForm extends Form {
 
     this.scrollBarEnabled = enableScrollBar
 
-    this.scrollBar = new ScrollBar(this)
+    this.scrollBar = new ScrollBar({
+      getLayoutType: () => this.layoutType,
+      getCurrentScroll: () => this.scrollItems,
+      getMaximumScroll: () => this.getScrollItemsLength(),
+      getTotalItems: () => this.inputs.length
+    })
     this.scrollBarShown = false
   }
 
@@ -297,108 +301,3 @@ module.exports = class ListScrollForm extends Form {
     }
   }
 }
-
-class ScrollBar extends DisplayElement {
-  constructor(listScrollForm) {
-    super()
-
-    this.listScrollForm = listScrollForm
-  }
-
-  fixLayout() {
-    // Normally we'd subtract one from contentW/contentH when setting the x/y
-    // position, but the scrollbar is actually displayed OUTSIDE of (adjacent
-    // to) the parent's content area.
-    if (this.listScrollForm.layoutType === 'vertical') {
-      this.h = this.listScrollForm.contentH
-      this.w = 1
-      this.x = this.listScrollForm.contentW
-      this.y = 0
-    } else {
-      this.h = 1
-      this.w = this.listScrollForm.contentW
-      this.x = 0
-      this.y = this.listScrollForm.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.
-
-    const currentScroll = this.listScrollForm.scrollItems
-    const edgeLength = this.listScrollForm.contentH
-    const totalItems = this.listScrollForm.inputs.length
-    const itemsVisibleAtOnce = Math.min(totalItems, edgeLength)
-    const handleLength = itemsVisibleAtOnce / 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.listScrollForm.layoutType === '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.listScrollForm.scrollItems
-    const totalScroll = this.listScrollForm.getScrollItemsLength()
-
-    return {
-      backwards: (currentScroll > 0),
-      forwards: (currentScroll < totalScroll)
-    }
-  }
-
-  canScrollAtAll() {
-    const {backwards, forwards} = this.getScrollableDirections()
-    return backwards || forwards
-  }
-}
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
+  }
+}