use unicode_width::UnicodeWidthChar;
pub const DEFAULT_TAB_WIDTH: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WrapMode {
None,
#[default]
Char,
Word,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WrapIndent {
#[default]
None,
SameAsLineIndent,
FixedCells(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WrapPoint {
pub char_index: usize,
pub byte_offset: usize,
}
#[derive(Debug, Clone)]
pub struct VisualLineInfo {
pub visual_line_count: usize,
pub wrap_points: Vec<WrapPoint>,
}
impl VisualLineInfo {
pub fn new() -> Self {
Self {
visual_line_count: 1,
wrap_points: Vec::new(),
}
}
pub fn from_text(text: &str, viewport_width: usize) -> Self {
let wrap_points = calculate_wrap_points(text, viewport_width);
let visual_line_count = wrap_points.len() + 1;
Self {
visual_line_count,
wrap_points,
}
}
pub fn from_text_with_tab_width(text: &str, viewport_width: usize, tab_width: usize) -> Self {
let wrap_points = calculate_wrap_points_with_tab_width(text, viewport_width, tab_width);
let visual_line_count = wrap_points.len() + 1;
Self {
visual_line_count,
wrap_points,
}
}
pub fn from_text_with_options(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_mode: WrapMode,
) -> Self {
Self::from_text_with_layout_options(
text,
viewport_width,
tab_width,
wrap_mode,
WrapIndent::None,
)
}
pub fn from_text_with_layout_options(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_mode: WrapMode,
wrap_indent: WrapIndent,
) -> Self {
let wrap_points = calculate_wrap_points_with_tab_width_mode_and_indent(
text,
viewport_width,
tab_width,
wrap_mode,
wrap_indent,
);
let visual_line_count = wrap_points.len() + 1;
Self {
visual_line_count,
wrap_points,
}
}
}
impl Default for VisualLineInfo {
fn default() -> Self {
Self::new()
}
}
pub fn char_width(ch: char) -> usize {
UnicodeWidthChar::width(ch).unwrap_or(1)
}
pub fn cell_width_at(ch: char, cell_offset_in_line: usize, tab_width: usize) -> usize {
if ch == '\t' {
let tab_width = tab_width.max(1);
let rem = cell_offset_in_line % tab_width;
tab_width - rem
} else {
char_width(ch)
}
}
pub fn str_width(s: &str) -> usize {
s.chars().map(char_width).sum()
}
pub fn str_width_with_tab_width(s: &str, tab_width: usize) -> usize {
let mut x = 0usize;
for ch in s.chars() {
x = x.saturating_add(cell_width_at(ch, x, tab_width));
}
x
}
pub fn visual_x_for_column(line: &str, column: usize, tab_width: usize) -> usize {
let mut x = 0usize;
for ch in line.chars().take(column) {
x = x.saturating_add(cell_width_at(ch, x, tab_width));
}
x
}
fn leading_whitespace_prefix_slice(line: &str) -> &str {
let bytes = line.as_bytes();
let mut end = 0usize;
while end < bytes.len() {
match bytes[end] {
b' ' | b'\t' => end += 1,
_ => break,
}
}
&line[..end]
}
pub(crate) fn wrap_indent_cells_for_line_text(
line_text: &str,
wrap_indent: WrapIndent,
viewport_width: usize,
tab_width: usize,
) -> usize {
if viewport_width <= 1 {
return 0;
}
let raw = match wrap_indent {
WrapIndent::None => 0,
WrapIndent::FixedCells(n) => n,
WrapIndent::SameAsLineIndent => {
let prefix = leading_whitespace_prefix_slice(line_text);
str_width_with_tab_width(prefix, tab_width)
}
};
raw.min(viewport_width.saturating_sub(1))
}
pub fn calculate_wrap_points(text: &str, viewport_width: usize) -> Vec<WrapPoint> {
calculate_wrap_points_with_tab_width(text, viewport_width, DEFAULT_TAB_WIDTH)
}
pub fn calculate_wrap_points_with_tab_width(
text: &str,
viewport_width: usize,
tab_width: usize,
) -> Vec<WrapPoint> {
calculate_wrap_points_with_tab_width_and_mode(text, viewport_width, tab_width, WrapMode::Char)
}
pub fn calculate_wrap_points_with_tab_width_and_mode(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_mode: WrapMode,
) -> Vec<WrapPoint> {
calculate_wrap_points_with_tab_width_mode_and_indent(
text,
viewport_width,
tab_width,
wrap_mode,
WrapIndent::None,
)
}
pub fn calculate_wrap_points_with_tab_width_mode_and_indent(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_mode: WrapMode,
wrap_indent: WrapIndent,
) -> Vec<WrapPoint> {
if viewport_width == 0 {
return Vec::new();
}
match wrap_mode {
WrapMode::None => Vec::new(),
WrapMode::Char => {
let indent =
wrap_indent_cells_for_line_text(text, wrap_indent, viewport_width, tab_width);
calculate_wrap_points_char_with_tab_width(text, viewport_width, tab_width, indent)
}
WrapMode::Word => {
let indent =
wrap_indent_cells_for_line_text(text, wrap_indent, viewport_width, tab_width);
calculate_wrap_points_word_with_tab_width(text, viewport_width, tab_width, indent)
}
}
}
fn calculate_wrap_points_char_with_tab_width(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_indent_cells: usize,
) -> Vec<WrapPoint> {
let mut wrap_points = Vec::new();
let mut x_in_segment = 0usize;
let mut x_in_line = 0usize;
for (char_index, (byte_offset, ch)) in text.char_indices().enumerate() {
let ch_width = cell_width_at(ch, x_in_line, tab_width);
if x_in_segment + ch_width > viewport_width {
wrap_points.push(WrapPoint {
char_index,
byte_offset,
});
x_in_segment = wrap_indent_cells;
} else {
}
x_in_segment = x_in_segment.saturating_add(ch_width);
x_in_line = x_in_line.saturating_add(ch_width);
if x_in_segment == viewport_width {
if byte_offset + ch.len_utf8() < text.len() {
wrap_points.push(WrapPoint {
char_index: char_index + 1,
byte_offset: byte_offset + ch.len_utf8(),
});
x_in_segment = wrap_indent_cells;
}
}
}
wrap_points
}
fn calculate_wrap_points_word_with_tab_width(
text: &str,
viewport_width: usize,
tab_width: usize,
wrap_indent_cells: usize,
) -> Vec<WrapPoint> {
let mut wrap_points = Vec::new();
let mut segment_start_char = 0usize;
let mut segment_start_x_in_line = 0usize;
let mut last_break: Option<(usize, usize, usize)> = None;
let mut x_in_line = 0usize;
for (char_index, (byte_offset, ch)) in text.char_indices().enumerate() {
let ch_width = cell_width_at(ch, x_in_line, tab_width);
loop {
let segment_indent = if segment_start_char == 0 {
0
} else {
wrap_indent_cells
};
let x_in_segment = x_in_line
.saturating_sub(segment_start_x_in_line)
.saturating_add(segment_indent);
if x_in_segment.saturating_add(ch_width) <= viewport_width {
break;
}
if let Some((break_char, break_byte, break_x)) = last_break
&& break_char > segment_start_char
{
wrap_points.push(WrapPoint {
char_index: break_char,
byte_offset: break_byte,
});
segment_start_char = break_char;
segment_start_x_in_line = break_x;
last_break = None;
continue;
}
wrap_points.push(WrapPoint {
char_index,
byte_offset,
});
segment_start_char = char_index;
segment_start_x_in_line = x_in_line;
last_break = None;
break;
}
x_in_line = x_in_line.saturating_add(ch_width);
if ch.is_whitespace() {
last_break = Some((char_index + 1, byte_offset + ch.len_utf8(), x_in_line));
}
}
wrap_points
}
pub struct LayoutEngine {
viewport_width: usize,
tab_width: usize,
wrap_mode: WrapMode,
wrap_indent: WrapIndent,
line_layouts: Vec<VisualLineInfo>,
}
impl LayoutEngine {
pub fn new(viewport_width: usize) -> Self {
Self {
viewport_width,
tab_width: DEFAULT_TAB_WIDTH,
wrap_mode: WrapMode::Char,
wrap_indent: WrapIndent::None,
line_layouts: Vec::new(),
}
}
pub fn set_viewport_width(&mut self, width: usize) {
if self.viewport_width != width {
self.viewport_width = width;
}
}
pub fn viewport_width(&self) -> usize {
self.viewport_width
}
pub fn wrap_mode(&self) -> WrapMode {
self.wrap_mode
}
pub fn set_wrap_mode(&mut self, wrap_mode: WrapMode) {
if self.wrap_mode != wrap_mode {
self.wrap_mode = wrap_mode;
}
}
pub fn wrap_indent(&self) -> WrapIndent {
self.wrap_indent
}
pub fn set_wrap_indent(&mut self, wrap_indent: WrapIndent) {
if self.wrap_indent != wrap_indent {
self.wrap_indent = wrap_indent;
}
}
pub fn tab_width(&self) -> usize {
self.tab_width
}
pub fn set_tab_width(&mut self, tab_width: usize) {
let tab_width = tab_width.max(1);
if self.tab_width != tab_width {
self.tab_width = tab_width;
}
}
pub fn from_lines(&mut self, lines: &[&str]) {
self.recalculate_all_from_lines(lines.iter().copied());
}
pub fn recalculate_all_from_lines<I, S>(&mut self, lines: I)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.line_layouts.clear();
for line in lines {
let line = line.as_ref();
self.line_layouts
.push(VisualLineInfo::from_text_with_layout_options(
line,
self.viewport_width,
self.tab_width,
self.wrap_mode,
self.wrap_indent,
));
}
}
pub fn add_line(&mut self, text: &str) {
self.line_layouts
.push(VisualLineInfo::from_text_with_layout_options(
text,
self.viewport_width,
self.tab_width,
self.wrap_mode,
self.wrap_indent,
));
}
pub fn update_line(&mut self, line_index: usize, text: &str) {
if line_index < self.line_layouts.len() {
self.line_layouts[line_index] = VisualLineInfo::from_text_with_layout_options(
text,
self.viewport_width,
self.tab_width,
self.wrap_mode,
self.wrap_indent,
);
}
}
pub fn insert_line(&mut self, line_index: usize, text: &str) {
let pos = line_index.min(self.line_layouts.len());
self.line_layouts.insert(
pos,
VisualLineInfo::from_text_with_layout_options(
text,
self.viewport_width,
self.tab_width,
self.wrap_mode,
self.wrap_indent,
),
);
}
pub fn delete_line(&mut self, line_index: usize) {
if line_index < self.line_layouts.len() {
self.line_layouts.remove(line_index);
}
}
pub fn get_line_layout(&self, line_index: usize) -> Option<&VisualLineInfo> {
self.line_layouts.get(line_index)
}
pub fn logical_line_count(&self) -> usize {
self.line_layouts.len()
}
pub fn visual_line_count(&self) -> usize {
self.line_layouts.iter().map(|l| l.visual_line_count).sum()
}
pub fn logical_to_visual_line(&self, logical_line: usize) -> usize {
self.line_layouts
.iter()
.take(logical_line)
.map(|l| l.visual_line_count)
.sum()
}
pub fn visual_to_logical_line(&self, visual_line: usize) -> (usize, usize) {
let mut cumulative_visual = 0;
for (logical_idx, layout) in self.line_layouts.iter().enumerate() {
if cumulative_visual + layout.visual_line_count > visual_line {
let visual_offset = visual_line - cumulative_visual;
return (logical_idx, visual_offset);
}
cumulative_visual += layout.visual_line_count;
}
let last_line = self.line_layouts.len().saturating_sub(1);
let last_visual_offset = self
.line_layouts
.last()
.map(|l| l.visual_line_count.saturating_sub(1))
.unwrap_or(0);
(last_line, last_visual_offset)
}
pub fn clear(&mut self) {
self.line_layouts.clear();
}
pub fn logical_position_to_visual(
&self,
logical_line: usize,
column: usize,
line_text: &str,
) -> Option<(usize, usize)> {
let layout = self.get_line_layout(logical_line)?;
let line_char_len = line_text.chars().count();
let column = column.min(line_char_len);
let mut wrapped_offset = 0usize;
let mut segment_start_col = 0usize;
for wrap_point in &layout.wrap_points {
if column >= wrap_point.char_index {
wrapped_offset += 1;
segment_start_col = wrap_point.char_index;
} else {
break;
}
}
let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
let mut x_in_line = seg_start_x_in_line;
let mut x_in_segment = 0usize;
for ch in line_text
.chars()
.skip(segment_start_col)
.take(column.saturating_sub(segment_start_col))
{
let w = cell_width_at(ch, x_in_line, self.tab_width);
x_in_line = x_in_line.saturating_add(w);
x_in_segment = x_in_segment.saturating_add(w);
}
let indent = if wrapped_offset == 0 {
0
} else {
wrap_indent_cells_for_line_text(
line_text,
self.wrap_indent,
self.viewport_width,
self.tab_width,
)
};
let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
Some((visual_row, indent.saturating_add(x_in_segment)))
}
pub fn logical_position_to_visual_allow_virtual(
&self,
logical_line: usize,
column: usize,
line_text: &str,
) -> Option<(usize, usize)> {
let layout = self.get_line_layout(logical_line)?;
let line_char_len = line_text.chars().count();
let clamped_column = column.min(line_char_len);
let mut wrapped_offset = 0usize;
let mut segment_start_col = 0usize;
for wrap_point in &layout.wrap_points {
if clamped_column >= wrap_point.char_index {
wrapped_offset += 1;
segment_start_col = wrap_point.char_index;
} else {
break;
}
}
let seg_start_x_in_line = visual_x_for_column(line_text, segment_start_col, self.tab_width);
let mut x_in_line = seg_start_x_in_line;
let mut x_in_segment = 0usize;
for ch in line_text
.chars()
.skip(segment_start_col)
.take(clamped_column.saturating_sub(segment_start_col))
{
let w = cell_width_at(ch, x_in_line, self.tab_width);
x_in_line = x_in_line.saturating_add(w);
x_in_segment = x_in_segment.saturating_add(w);
}
let indent = if wrapped_offset == 0 {
0
} else {
wrap_indent_cells_for_line_text(
line_text,
self.wrap_indent,
self.viewport_width,
self.tab_width,
)
};
let x_in_segment = x_in_segment + column.saturating_sub(line_char_len);
let visual_row = self.logical_to_visual_line(logical_line) + wrapped_offset;
Some((visual_row, indent.saturating_add(x_in_segment)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_char_width() {
assert_eq!(char_width('a'), 1);
assert_eq!(char_width('A'), 1);
assert_eq!(char_width(' '), 1);
assert_eq!(char_width('你'), 2);
assert_eq!(char_width('好'), 2);
assert_eq!(char_width('世'), 2);
assert_eq!(char_width('界'), 2);
assert_eq!(char_width('👋'), 2);
assert_eq!(char_width('🌍'), 2);
assert_eq!(char_width('🦀'), 2);
}
#[test]
fn test_str_width() {
assert_eq!(str_width("hello"), 5);
assert_eq!(str_width("你好"), 4); assert_eq!(str_width("hello你好"), 9); assert_eq!(str_width("👋🌍"), 4); }
#[test]
fn test_tab_width_expansion() {
assert_eq!(cell_width_at('\t', 0, 4), 4);
assert_eq!(cell_width_at('\t', 1, 4), 3);
assert_eq!(cell_width_at('\t', 2, 4), 2);
assert_eq!(cell_width_at('\t', 3, 4), 1);
assert_eq!(cell_width_at('\t', 4, 4), 4);
assert_eq!(str_width_with_tab_width("\t", 4), 4);
assert_eq!(str_width_with_tab_width("a\t", 4), 4); assert_eq!(str_width_with_tab_width("ab\t", 4), 4); assert_eq!(str_width_with_tab_width("abc\t", 4), 4); assert_eq!(str_width_with_tab_width("abcd\t", 4), 8); }
#[test]
fn test_calculate_wrap_points_simple() {
let text = "hello world";
let wraps = calculate_wrap_points(text, 10);
assert!(!wraps.is_empty());
}
#[test]
fn test_calculate_wrap_points_exact_fit() {
let text = "1234567890";
let wraps = calculate_wrap_points(text, 10);
assert_eq!(wraps.len(), 0);
}
#[test]
fn test_calculate_wrap_points_one_over() {
let text = "12345678901";
let wraps = calculate_wrap_points(text, 10);
assert_eq!(wraps.len(), 1);
assert_eq!(wraps[0].char_index, 10);
}
#[test]
fn test_calculate_wrap_points_cjk() {
let text = "你好世界测";
let wraps = calculate_wrap_points(text, 10);
assert_eq!(wraps.len(), 0);
}
#[test]
fn test_calculate_wrap_points_cjk_overflow() {
let text = "你好世界测试";
let wraps = calculate_wrap_points(text, 10);
assert_eq!(wraps.len(), 1);
assert_eq!(wraps[0].char_index, 5);
}
#[test]
fn test_wrap_mode_none_disables_wrapping() {
let mut engine = LayoutEngine::new(5);
engine.set_wrap_mode(WrapMode::None);
engine.from_lines(&["abcdefghij"]);
assert_eq!(engine.visual_line_count(), 1);
let layout = engine.get_line_layout(0).expect("layout");
assert_eq!(layout.visual_line_count, 1);
assert!(layout.wrap_points.is_empty());
}
#[test]
fn test_word_wrap_prefers_whitespace_when_possible() {
let text = "hello world";
let wraps = calculate_wrap_points_with_tab_width_and_mode(
text,
7,
DEFAULT_TAB_WIDTH,
WrapMode::Word,
);
assert_eq!(wraps.len(), 1);
assert_eq!(wraps[0].char_index, 6);
}
#[test]
fn test_wrap_indent_same_as_line_indent_reduces_continuation_width() {
let text = " abcdefgh";
let wraps = calculate_wrap_points_with_tab_width_mode_and_indent(
text,
6,
DEFAULT_TAB_WIDTH,
WrapMode::Char,
WrapIndent::SameAsLineIndent,
);
let indices: Vec<usize> = wraps.iter().map(|wp| wp.char_index).collect();
assert_eq!(indices, vec![6, 8, 10]);
}
#[test]
fn test_wrap_double_width_char() {
let text = "Hello你";
let wraps = calculate_wrap_points(text, 6);
assert_eq!(wraps.len(), 1);
assert_eq!(wraps[0].char_index, 5); }
#[test]
fn test_visual_line_info() {
let info = VisualLineInfo::from_text("1234567890abc", 10);
assert_eq!(info.visual_line_count, 2); assert_eq!(info.wrap_points.len(), 1);
}
#[test]
fn test_layout_engine_basic() {
let mut engine = LayoutEngine::new(10);
engine.add_line("hello");
engine.add_line("1234567890abc");
assert_eq!(engine.logical_line_count(), 2);
assert_eq!(engine.visual_line_count(), 3); }
#[test]
fn test_layout_engine_viewport_change() {
let mut engine = LayoutEngine::new(20);
engine.from_lines(&["hello world", "rust programming"]);
let initial_visual = engine.visual_line_count();
assert_eq!(initial_visual, 2);
engine.set_viewport_width(5);
engine.from_lines(&["hello world", "rust programming"]);
let new_visual = engine.visual_line_count();
assert!(new_visual > initial_visual); }
#[test]
fn test_logical_to_visual() {
let mut engine = LayoutEngine::new(10);
engine.from_lines(&["12345", "1234567890abc", "hello"]);
assert_eq!(engine.logical_to_visual_line(0), 0);
assert_eq!(engine.logical_to_visual_line(1), 1);
assert_eq!(engine.logical_to_visual_line(2), 3);
}
#[test]
fn test_visual_to_logical() {
let mut engine = LayoutEngine::new(10);
engine.from_lines(&["12345", "1234567890abc", "hello"]);
assert_eq!(engine.visual_to_logical_line(0), (0, 0));
assert_eq!(engine.visual_to_logical_line(1), (1, 0));
assert_eq!(engine.visual_to_logical_line(2), (1, 1));
assert_eq!(engine.visual_to_logical_line(3), (2, 0));
}
}