« get me outta code hell

ScrollBar.js « form « ui - 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
blob: 13ba7fea45646b1489ebfd5f5507e3ece5b8fd69 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
  }
}