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
|
// Laying the box you type in out as rows: a long line wraps at spaces rather
// than running off the edge and taking the cursor with it.
package acp
// inputRow is one row of the box as drawn: which typed line it shows, and
// which of that line's runes, as a half-open range.
//
// The typed lines stay the model — Alt-Enter makes one, Backspace at a line
// start joins two — and wrapping is only how they are shown. A row is never
// stored: it is recomputed from the lines and the width at every frame and
// every key, so a resize cannot leave the two out of step.
type inputRow struct {
line int
start, end int
}
// inputWidth is how many runes a row of the box holds: the prompt, a space,
// and one column kept free so the cursor sitting just past a full row is
// still inside the pane rather than on the frame.
func (v *View) inputWidth() int {
return max(v.Bounds().W-3, 1)
}
// inputRows lays every typed line out at a width.
func (v *View) inputRows(width int) []inputRow {
var rows []inputRow
for line, text := range v.input {
rows = append(rows, wrapInputLine(line, []rune(text), width)...)
}
return rows
}
// wrapInputLine breaks one typed line into rows no wider than width, at the
// last space that fits and mid-word when a word is longer than a row.
//
// The space a row breaks at stays at the end of that row rather than opening
// the next, so the runes of the line are exactly the rows laid end to end and
// a cursor position is one row's and no other's. A row may therefore hold
// width+1 runes, the last a space, which is not drawn.
//
// A line that ends in such a space — you have just typed it — is followed by
// an empty row, which falls out of the loop: what is left after the break is
// nothing, and nothing is a row. That is where the cursor then sits.
func wrapInputLine(line int, runes []rune, width int) []inputRow {
var rows []inputRow
start := 0
for {
remaining := runes[start:]
if len(remaining) <= width {
return append(rows, inputRow{line: line, start: start, end: len(runes)})
}
at := start + inputBreak(remaining, width)
rows = append(rows, inputRow{line: line, start: start, end: at})
start = at
}
}
// inputBreak returns how many runes the first row of a run longer than width
// takes: through the last space in the first width+1 runes, or width when
// there is none.
func inputBreak(runes []rune, width int) int {
for at := width; at >= 0; at-- {
if runes[at] == ' ' {
return at + 1
}
}
return width
}
// inputCursor returns which row the cursor is on and how far along it.
//
// A position at the very end of a row that continues belongs to the next
// row's start, which is what makes typing across a wrap look like typing.
func (v *View) inputCursor(rows []inputRow) (row, column int) {
for i, r := range rows {
if r.line != v.cursor {
continue
}
last := i == len(rows)-1 || rows[i+1].line != r.line
if v.column < r.end || (last && v.column <= r.end) {
return i, v.column - r.start
}
}
return max(len(rows)-1, 0), 0
}
// moveInputRow moves the cursor to the row above or below the one it is on,
// keeping its column where the row allows, and reports whether there was one.
//
// Rows, not lines: a paragraph that wraps three times is three rows to the
// eye, and Up from its last row should land on its middle, not on the
// paragraph before.
func (v *View) moveInputRow(by int) bool {
rows := v.inputRows(v.inputWidth())
row, column := v.inputCursor(rows)
target := row + by
if target < 0 || target >= len(rows) {
return false
}
r := rows[target]
widest := r.end - r.start
if last := target == len(rows)-1 || rows[target+1].line != r.line; !last {
widest-- // the row's last position is the next row's first
}
v.cursor = r.line
v.column = r.start + min(column, max(widest, 0))
v.leaveToken()
return true
}
|