turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

input.go · 111 lines · 3.8 KBGo Blame HistoryRaw
  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
}