//! Where the characters of an entry's text land on the cells of its field. //! //! One answer, used by everything that has a question about a text field: the //! painter draws these lines, the caret is drawn at the cell this says it is //! on, a click is turned back into a character index by asking which line a //! row is, and up and down are a step through this list. They cannot disagree, //! because there is only the one layout. //! //! It is written in characters rather than in glyphs on purpose: the caret is //! an index into the text, which is what an edit is made against, and columns //! are worked out from the characters rather than the other way round. A wide //! glyph is two cells and one index, and both halves of that are true here. use crate::screen; /// One visual row of a field: the half-open range of characters on it. /// /// A row ends either at a hard newline — the newline itself is not part of the /// range — or where the wrap fell, in which case the space that was wrapped on /// *is* part of the range and is not drawn. That is what makes a caret just /// past a wrapped space belong to the row below rather than hanging off the /// end of the row above, which is the behaviour every editor has. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Line { pub start: usize, pub end: usize, } /// How a field's text sits in the rect it was given. #[derive(Clone, Debug)] pub struct View { /// The characters, so callers do not re-collect them. pub text: Vec, pub lines: Vec, /// The first row painted — a multi-line field taller than its rect scrolls /// down to keep the caret on screen. pub top: usize, /// The first *column* painted on each row. Only a single-line field ever /// has one: a field that wraps has nothing to scroll sideways to. pub left: usize, /// How many rows the rect holds. pub rows: usize, } fn char_cols(ch: char) -> u16 { if ch == '\n' { 0 } else { screen::glyph_cols(&ch.to_string()) } } /// The columns `text[from..to]` takes. pub fn cols(text: &[char], from: usize, to: usize) -> u16 { text[from.min(text.len())..to.min(text.len())] .iter() .map(|ch| char_cols(*ch)) .sum() } /// Break one paragraph — a run with no newline in it — into rows of `width` /// columns, on spaces where it can and mid-word where it must. fn wrap_paragraph(text: &[char], start: usize, end: usize, width: u16, out: &mut Vec) { let width = width.max(1); let mut at = start; loop { let mut used = 0u16; let mut to = at; // The character after the last space that fits, which is where the row // breaks when a break on a space is possible at all. let mut after_space = None; while to < end { let next = used.saturating_add(char_cols(text[to])); if next > width { break; } used = next; to += 1; if text[to - 1] == ' ' { after_space = Some(to); } } if to >= end { out.push(Line { start: at, end }); return; } // Something is left over, so this row has to break. On the space when // there was one — but never on a space that is the whole row, or a // paragraph of spaces would never advance. let brk = match after_space { Some(space) if space > at => space, _ => to.max(at + 1), }; out.push(Line { start: at, end: brk, }); at = brk; } } /// Lay `text` out in a field `width` columns across. /// /// A single-line field is one row however long the text is; the sideways /// scroll in [`View::left`] is what keeps the caret on screen there. pub fn lines(text: &[char], width: u16, multiline: bool) -> Vec { if !multiline { return vec![Line { start: 0, end: text.len(), }]; } let mut out = Vec::new(); let mut at = 0usize; for (i, ch) in text.iter().enumerate() { if *ch == '\n' { wrap_paragraph(text, at, i, width, &mut out); at = i + 1; } } wrap_paragraph(text, at, text.len(), width, &mut out); out } impl View { /// The whole answer for one field: its rows, and how far they are scrolled /// so that `caret` is on screen. /// /// `top` is where the field was scrolled to on the last frame. Keeping it /// is what stops a field jumping about: it moves only when the caret has /// gone off one end, and then only far enough to bring it back. pub fn of( text: &str, width: u16, height: u16, multiline: bool, caret: usize, was_top: usize, ) -> Self { let chars: Vec = text.chars().collect(); let width = width.max(1); let rows = height.max(1) as usize; let lines = lines(&chars, width, multiline); let caret = caret.min(chars.len()); let row = row_of(&lines, caret); let last = lines.len().saturating_sub(rows); let mut top = was_top.min(last); if row < top { top = row; } else if row >= top + rows { top = row + 1 - rows; } // Sideways, for the one line a single-line field has. The window is a // column short of the field so the caret at the end of the text has a // cell of its own to sit in. let window = width.saturating_sub(1).max(1); let mut left = 0usize; if !multiline { while cols(&chars, left, caret) > window { left += 1; } } Self { text: chars, lines, top, left, rows, } } /// The row `at` is on, and how many columns into it it sits. pub fn caret_at(&self, at: usize) -> (usize, u16) { let row = row_of(&self.lines, at.min(self.text.len())); let from = self.skip(self.lines[row]); (row, cols(&self.text, from, at.max(from))) } /// What is drawn on `row`: its text, with the space a wrap ate left off, /// and — on a single-line field — the columns scrolled off the left. pub fn painted(&self, row: usize) -> String { let Some(line) = self.lines.get(row) else { return String::new(); }; let end = self.line_end(row); let from = self.skip(*line).min(end); self.text[from..end].iter().collect() } /// The character a click on this cell of the field means. /// /// `x` and `y` are relative to the field's own top-left corner. A click /// below the last row of text is the end of the text and a click past the /// end of a row is the end of that row, which is what makes clicking into /// the empty half of a half-full field land somewhere sensible. pub fn hit(&self, x: u16, y: u16) -> usize { let row = self.top + y as usize; if row >= self.lines.len() { return self.text.len(); } let line = self.lines[row]; let mut at = self.skip(line); let mut used = 0u16; while at < line.end { let w = char_cols(self.text[at]); if used + w > x { break; } used += w; at += 1; } // Not onto the space a wrap ate: that cell is the end of this row, and // the character after it starts the next one. self.line_end(row).min(at) } /// The first character drawn on a row — the sideways scroll, which only a /// single-line field has. fn skip(&self, line: Line) -> usize { line.start.max(self.left.min(line.end)) } /// The first and last character of the row `at` is on — what Home and End /// mean in a box of text, and what a kill to the start or the end of the /// line takes. pub fn caret_row(&self, at: usize) -> (usize, usize) { let row = row_of(&self.lines, at.min(self.text.len())); (self.lines[row].start, self.line_end(row)) } /// Where the text on `row` ends, not counting the space a wrap ate. pub fn line_end(&self, row: usize) -> usize { let Some(line) = self.lines.get(row) else { return self.text.len(); }; let wrapped = row + 1 < self.lines.len() && self.lines[row + 1].start == line.end; if wrapped && line.end > line.start && self.text[line.end - 1] == ' ' { line.end - 1 } else { line.end } } /// The character at `col` columns into `row` — how up and down keep their /// place across rows of different lengths. pub fn at_col(&self, row: usize, col: u16) -> usize { let row = row.min(self.lines.len().saturating_sub(1)); let line = self.lines[row]; let mut at = line.start; let mut used = 0u16; while at < line.end && used < col { used += char_cols(self.text[at]); at += 1; } self.line_end(row).min(at) } } /// Which row holds `at`. /// /// The last row that starts at or before it, so a caret exactly on a break /// belongs to the row below — which is where the next character typed goes. pub fn row_of(lines: &[Line], at: usize) -> usize { lines.iter().rposition(|line| line.start <= at).unwrap_or(0) } #[cfg(test)] mod tests { use super::*; fn view(text: &str, width: u16, height: u16, caret: usize) -> View { View::of(text, width, height, true, caret, 0) } #[test] fn a_wrapped_row_ends_after_the_space_it_broke_on() { let v = view("one two three", 8, 3, 0); assert_eq!(v.painted(0), "one two"); assert_eq!(v.painted(1), "three"); // The space is on the first row's range but is not drawn, so a caret // just past it is at the start of the second row. assert_eq!(v.caret_at(8), (1, 0)); } #[test] fn a_newline_is_a_row_of_its_own_even_when_it_is_empty() { let v = view("a\n\nb", 8, 4, 0); assert_eq!(v.lines.len(), 3); assert_eq!(v.painted(1), ""); assert_eq!(v.caret_at(2), (1, 0)); assert_eq!(v.caret_at(3), (2, 0)); } #[test] fn a_click_lands_on_the_character_under_it() { let v = view("one two three", 8, 3, 0); assert_eq!(v.hit(0, 0), 0); assert_eq!(v.hit(4, 0), 4); // Past the end of a row is the end of that row, not the row below. assert_eq!(v.hit(20, 0), 7); assert_eq!(v.hit(2, 1), 10); // Below the text is the end of it. assert_eq!(v.hit(0, 7), 13); } #[test] fn a_caret_off_the_bottom_scrolls_the_field_by_one_row() { let text = "a\nb\nc\nd"; let v = View::of(text, 8, 2, true, 6, 0); assert_eq!(v.top, 2, "two rows of four, with the caret on the last"); // And it stays where it was while the caret is still on screen. let v = View::of(text, 8, 2, true, 4, 2); assert_eq!(v.top, 2); } #[test] fn a_single_line_field_scrolls_sideways_to_the_caret() { let v = View::of("abcdefghij", 5, 1, false, 10, 0); assert_eq!(v.lines.len(), 1); assert_eq!(v.painted(0), "ghij"); assert_eq!(v.caret_at(10), (0, 4)); assert_eq!(v.hit(0, 0), 6); } #[test] fn a_wide_glyph_is_two_cells_and_one_character() { let v = view("a😀b", 8, 2, 0); assert_eq!(v.caret_at(2), (0, 3)); assert_eq!(v.hit(1, 0), 1); assert_eq!(v.hit(2, 0), 1, "the second cell is still the emoji"); assert_eq!(v.hit(3, 0), 2); } }