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