-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscrolltextview.go
More file actions
319 lines (282 loc) · 7.74 KB
/
scrolltextview.go
File metadata and controls
319 lines (282 loc) · 7.74 KB
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
package startprompt
import (
"strings"
"golang.design/x/clipboard"
)
type xChar struct {
*Char
x int
}
type sScrollTextView struct {
// 选中文本
selectionText string
// 行列二维数组
data [][]xChar
// 当前输入左上角在 data 第几行
inputY int
// data 在 y 轴上的偏移量(滚动量)
// 其实就是从第几行开始显示在窗口中
// 范围在 [0, offsetLimitY]
// offsetY 之所以有个上界,是为了模拟终端的滚动效果,
// 终端的滚动条默认无法移动,按下 Ctrl-L 置顶当前输入
// 此时滚动条可以向上移动,向下只能移动到最初的位置
offsetY int
// 偏移量的最大值
offsetLimitY int
// 选中区域
selection area
}
func newScrollTextView() *sScrollTextView {
return &sScrollTextView{data: [][]xChar{nil}}
}
// growTo 增加数据长度, y 是从 0 开始的索引
func (st *sScrollTextView) growTo(y int) {
for i := len(st.data) - 1; i < y; i++ {
st.data = append(st.data, []xChar{})
}
}
func (st *sScrollTextView) appendAt(vy int, xchar xChar) {
st.data[vy] = append(st.data[vy], xchar)
}
func (st *sScrollTextView) readScreen(screen *Screen) {
lastCoordinate := screen.getLastCoordinate()
buffer := screen.GetBuffer()
// 清空之前的输入数据
st.data = st.data[:st.inputY]
st.growTo(st.inputY + lastCoordinate.Y)
for y := 0; y <= lastCoordinate.Y; y++ {
vy := st.inputY + y
lineBuffer, found := buffer[y]
if found {
// 当前行最大的 x 坐标
endX := 0
for x := range lineBuffer {
if x > endX {
endX = x
}
}
x := 0
for x <= endX {
var char *Char
if _, found := lineBuffer[x]; found {
char = lineBuffer[x]
} else {
char = newChar(' ', nil)
}
st.appendAt(vy, xChar{char, x})
x += char.width()
}
}
}
}
// getLineAt 传入窗口坐标 y ,返回对应行数据
func (st *sScrollTextView) getLineAt(y int) ([]xChar, bool) {
vy := st.offsetY + y
return st.getLine(vy)
}
func (st *sScrollTextView) getLine(n int) ([]xChar, bool) {
if n <= len(st.data)-1 {
return st.data[n], true
}
return nil, false
}
func (st *sScrollTextView) getLastCoordinate() Coordinate {
y := len(st.data) - 1
lastLine := st.data[y]
x := 0
if len(lastLine) > 0 {
ch := lastLine[len(lastLine)-1]
x = ch.x + ch.width()
}
return Coordinate{x, y}
}
// restoreScroll 恢复原本的滚动位置
//
// 当我们滚动到之前的文本时,按下键盘,画面应该回到之前输入的位置。
// 效果参考终端
func (st *sScrollTextView) restoreScroll() {
st.offsetY = st.offsetLimitY
}
// moveUp 文本向上移动,会增加滚动的边界
func (st *sScrollTextView) moveUp(n int) int {
if st.offsetLimitY+n > len(st.data)-1 {
n = len(st.data) - 1 - st.offsetLimitY
}
st.offsetY += n
st.offsetLimitY += n
return n
}
// moveDown 文本向下移动,会减少滚动的边界
func (st *sScrollTextView) moveDown(n int) int {
if st.offsetLimitY < n {
n = st.offsetLimitY
}
st.offsetY -= n
st.offsetLimitY -= n
return n
}
// scrollUp 文本向上滚动
func (st *sScrollTextView) scrollUp(n int) int {
if st.offsetY+n > st.offsetLimitY {
n = st.offsetLimitY - st.offsetY
}
st.offsetY += n
return n
}
// scrollDown 文本向下滚动,返回实际滚动行数
func (st *sScrollTextView) scrollDown(n int) int {
if n > st.offsetY {
n = st.offsetY
}
st.offsetY -= n
return n
}
func (st *sScrollTextView) inputToEnd() {
st.inputY = len(st.data) - 1
}
func (st *sScrollTextView) acceptInput() {
st.inputY = len(st.data)
st.growTo(st.inputY)
}
// containLine 是否包含窗口坐标 y 处行
func (st *sScrollTextView) containLine(y int) bool {
sy := st.offsetY + y
return sy < len(st.data)
}
// inputContainLine 当前输入是否包含窗口坐标 y 处行
func (st *sScrollTextView) inputContainLine(y int) bool {
sy := st.offsetY + y
return st.inputY <= sy && sy < len(st.data)
}
// getInputStartCoordinate 返回当前输入左上角的窗口坐标
func (st *sScrollTextView) getInputStartCoordinate() Coordinate {
return Coordinate{0, st.inputY - st.offsetY}
}
// getClosetCharCoordinate 返回最接近的字符窗口坐标,布尔值表示是否找到
func (st *sScrollTextView) getClosetCharCoordinate(windowCoordinate Coordinate) (Coordinate, bool) {
lineData, found := st.getLineAt(windowCoordinate.Y)
if !found {
return Coordinate{-1, -1}, false
}
ret := Coordinate{0, windowCoordinate.Y}
// 找到最后一个 x 坐标小于等于的
for _, datum := range lineData {
if datum.x > windowCoordinate.X {
return ret, true
}
ret.X = datum.x
}
return Coordinate{-1, -1}, false
}
// getWordArea 返回坐标处的单词区域
func (st *sScrollTextView) getWordArea(coordinate Coordinate) area {
lineData, found := st.getLine(coordinate.Y)
if !found {
return area{}
}
// 找到窗口坐标所在字符索引
index := -1
for i, datum := range lineData {
if datum.x > coordinate.X {
break
}
index = i
}
if index == -1 {
return area{}
}
length := len(lineData)
// 默认是行尾
end := Coordinate{lineData[length-1].x + lineData[length-1].width(), coordinate.Y}
for i := index; i < length; i++ {
xc := lineData[i]
if IsSpace(xc.char) {
end = Coordinate{xc.x, coordinate.Y}
break
}
}
// 默认是行首
start := Coordinate{lineData[0].x, coordinate.Y}
for i := index; i >= 0; i-- {
xc := lineData[i]
if IsSpace(xc.char) {
start = Coordinate{xc.x + xc.width(), coordinate.Y}
break
}
}
return area{
start: start,
end: end,
}
}
// mouseDown 鼠标(左键)点击,传入窗口坐标
func (st *sScrollTextView) mouseDown(windowCoordinate Coordinate) {
coor := st.convertWindowCoordinate(windowCoordinate)
st.selection = area{start: coor, end: coor}
}
func (st *sScrollTextView) mouseMove(windowCoordinate Coordinate) {
coor := st.convertWindowCoordinate(windowCoordinate)
st.selection.end = coor
}
func (st *sScrollTextView) mouseUp(windowCoordinate Coordinate) {
coor := st.convertWindowCoordinate(windowCoordinate)
st.selection.end = coor
// 如果坐标超出范围,将其设置为最后一个坐标
last := st.getLastCoordinate()
st.selection.limitTo(last)
}
func (st *sScrollTextView) dblclick(windowCoordinate Coordinate) {
coor := st.convertWindowCoordinate(windowCoordinate)
st.selection = st.getWordArea(coor)
}
func (st *sScrollTextView) tripeClick(windowCoordinate Coordinate) {
coor := st.convertWindowCoordinate(windowCoordinate)
st.selection = area{
start: Coordinate{0, coor.Y},
end: Coordinate{1 << 24, coor.Y},
}
}
// inSelection 判断窗口坐标是否在选中区域内
func (st *sScrollTextView) inSelection(windowCoordinate Coordinate) bool {
coor := st.convertWindowCoordinate(windowCoordinate)
return st.selection.Contains(coor)
}
func (st *sScrollTextView) getSelectionText() string {
var builder strings.Builder
start := st.selection.getStart()
end := st.selection.getEnd()
for y := start.Y; y <= end.Y; y++ {
lineData, found := st.getLine(y)
if found {
for _, datum := range lineData {
if st.selection.Contains(Coordinate{datum.x, y}) {
builder.WriteString(datum.char)
}
}
}
if y != end.Y {
builder.WriteByte('\n')
}
}
return builder.String()
}
func (st *sScrollTextView) cancelSelection() {
st.selection = area{}
}
func (st *sScrollTextView) convertWindowCoordinate(windowCoordinate Coordinate) Coordinate {
coor := Coordinate{windowCoordinate.X, windowCoordinate.Y}
coor.addY(st.offsetY)
return coor
}
func (st *sScrollTextView) update() {
if st.selection.isEmpty() {
return
}
// 将选中文本复制到系统剪贴板
// 文本发生变化时复制一次
text := st.getSelectionText()
if text != st.selectionText {
clipboard.Write(clipboard.FmtText, []byte(text))
st.selectionText = text
}
}