91 lines
2.9 KiB
Rust
91 lines
2.9 KiB
Rust
#![allow(clippy::cast_possible_truncation)]
|
|
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ScreenPos {
|
|
pub row: u16,
|
|
pub col: u16,
|
|
}
|
|
|
|
pub fn build_cursor_map(text: &str, width: u16) -> Vec<ScreenPos> {
|
|
assert!(width > 0);
|
|
let width = width as usize;
|
|
let mut pos_map = vec![ScreenPos { row: 0, col: 0 }; text.len() + 1];
|
|
let mut row = 0;
|
|
let mut col = 0;
|
|
|
|
let mut word_start_idx = 0;
|
|
let mut word_start_col = 0;
|
|
|
|
for (byte_offset, grapheme) in text.grapheme_indices(true) {
|
|
let grapheme_width = UnicodeWidthStr::width(grapheme);
|
|
|
|
if grapheme == "\n" {
|
|
row += 1;
|
|
col = 0;
|
|
word_start_col = 0;
|
|
word_start_idx = byte_offset + grapheme.len();
|
|
// Set position for the end of this grapheme and any intermediate bytes
|
|
let end_pos = ScreenPos {
|
|
row: row as u16,
|
|
col: col as u16,
|
|
};
|
|
for i in 1..=grapheme.len() {
|
|
if byte_offset + i < pos_map.len() {
|
|
pos_map[byte_offset + i] = end_pos;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if grapheme.chars().all(char::is_whitespace) {
|
|
if col + grapheme_width > width {
|
|
// Whitespace causes wrap
|
|
row += 1;
|
|
col = 1; // Position after wrapping space
|
|
word_start_col = 1;
|
|
word_start_idx = byte_offset + grapheme.len();
|
|
} else {
|
|
col += grapheme_width;
|
|
word_start_col = col;
|
|
word_start_idx = byte_offset + grapheme.len();
|
|
}
|
|
} else if col + grapheme_width > width {
|
|
if word_start_col > 0 && byte_offset == word_start_idx {
|
|
// This is the first character of a new word that won't fit, wrap it
|
|
row += 1;
|
|
col = grapheme_width;
|
|
} else if word_start_col == 0 {
|
|
// No previous word boundary, hard break
|
|
row += 1;
|
|
col = grapheme_width;
|
|
} else {
|
|
// This is part of a word already on the line, let it extend beyond width
|
|
col += grapheme_width;
|
|
}
|
|
} else {
|
|
col += grapheme_width;
|
|
}
|
|
|
|
// Set position for the end of this grapheme and any intermediate bytes
|
|
let end_pos = ScreenPos {
|
|
row: row as u16,
|
|
col: col as u16,
|
|
};
|
|
for i in 1..=grapheme.len() {
|
|
if byte_offset + i < pos_map.len() {
|
|
pos_map[byte_offset + i] = end_pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
pos_map
|
|
}
|
|
|
|
pub fn byte_to_screen_pos(text: &str, byte_idx: usize, width: u16) -> ScreenPos {
|
|
let pos_map = build_cursor_map(text, width);
|
|
pos_map[byte_idx.min(text.len())]
|
|
}
|