#![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 { 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())] }