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
}
}
|