/
scroll.go
165 lines (142 loc) · 5.39 KB
/
scroll.go
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package text
// scroll.go contains code that tracks the current scrolling position.
import "math"
// scrollTracker tracks the current scrolling position for the Text widget.
//
// The text widget displays the contained text buffer as lines of text that fit
// the widget's canvas. The main goal of this object is to inform the text
// widget which should be the first drawn line from the buffer. This depends on
// two things, the scrolling position based on user inputs and whether the text
// widget is configured to roll the content up as new text is added by the
// client.
//
// The rolling Vs. scrolling state is tracked in an FSM implemented in this
// file.
//
// The client can scroll the content by either a keyboard or a mouse event. The
// widget receives these events concurrently with requests to redraw the
// content, so this objects keeps a track of all the scrolling events that
// happened since the last redraw and consumes them when calculating which is
// the first drawn line on the next redraw event.
//
// This is not thread safe.
type scrollTracker struct {
// scroll stores user requests to scroll up (negative) or down (positive).
// E.g. -1 means up by one line and 2 means down by two lines.
scroll int
// scrollPage stores user requests to scroll up (negative) or down
// (positive) by a page of content. E.g. -1 means up by one page and 2
// means down by two pages.
scrollPage int
// first tracks the first line that will be printed.
first int
// state is the state of the scrolling FSM.
state rollState
}
// newScrollTracker returns a new scroll tracker.
func newScrollTracker(opts *options) *scrollTracker {
if opts.rollContent {
return &scrollTracker{state: rollToEnd}
}
return &scrollTracker{state: rollingDisabled}
}
// upOneLine processes a user request to scroll up by one line.
func (st *scrollTracker) upOneLine() {
st.scroll--
}
// downOneLine processes a user request to scroll down by one line.
func (st *scrollTracker) downOneLine() {
st.scroll++
}
// upOnePage processes a user request to scroll up by one page.
func (st *scrollTracker) upOnePage() {
st.scrollPage--
}
// downOnePage processes a user request to scroll down by one page.
func (st *scrollTracker) downOnePage() {
st.scrollPage++
}
// doScroll processes any outstanding scroll requests and calculates the
// resulting first line.
func (st *scrollTracker) doScroll(lines, height int) int {
first := st.first + st.scroll + st.scrollPage*height
st.scroll = 0
st.scrollPage = 0
return normalizeScroll(first, lines, height)
}
// firstLine returns the number of the first line that should be drawn on a
// canvas of the specified height if there is the provided number of lines of
// text.
func (st *scrollTracker) firstLine(lines, height int) int {
// Execute the scrolling FSM.
st.state = st.state(st, lines, height)
return st.first
}
// rollState is a state in the scrolling FSM.
type rollState func(st *scrollTracker, lines, height int) rollState
// rollingDisabled is a state where content rolling was disabled by the
// configuration of the Text widget.
func rollingDisabled(st *scrollTracker, lines, height int) rollState {
st.first = st.doScroll(lines, height)
return rollingDisabled
}
// rollToEnd is a state in which the last line of the content is always
// visible. When new content arrives, it is rolled upwards.
func rollToEnd(st *scrollTracker, lines, height int) rollState {
// If the user didn't scroll, just roll the content so that the last line
// is visible.
if st.scroll == 0 && st.scrollPage == 0 {
st.first = normalizeScroll(math.MaxInt32, lines, height)
return rollToEnd
}
st.first = st.doScroll(lines, height)
if lastLineVisible(st.first, lines, height) {
return rollToEnd
}
return rollingPaused
}
// rollingPaused is a state in which the user scrolled up and made the last
// line scroll out of the view, so the content rolling is paused.
func rollingPaused(st *scrollTracker, lines, height int) rollState {
st.first = st.doScroll(lines, height)
if lastLineVisible(st.first, lines, height) {
return rollToEnd
}
return rollingPaused
}
// lastLineVisible returns true if the last text line from within the buffer of
// the text widget is visible on the canvas when drawing of the text starts
// from the specified start line, there is the provided total amount of lines
// and the canvas has the height.
func lastLineVisible(start, lines, height int) bool {
return lines-start <= height
}
// normalizeScroll returns normalized position of the first line that should be
// drawn when drawing the specified number of lines on a canvas with the
// provided height.
func normalizeScroll(first, lines, height int) int {
if first < 0 || lines <= 0 || height <= 0 {
return 0
}
if lines <= height {
return 0 // Scrolling not necessary if the content fits.
}
max := lines - height
if first > max {
return max
}
return first
}