Refactor TUI line wrapping: implement manual wrapping for consistency, preserve cursor styles, and handle wide characters.

This commit is contained in:
2025-09-29 23:05:08 +02:00
parent 6ca30f4176
commit 004fc0ba5e

View File

@@ -101,11 +101,31 @@ fn render_editable_textarea(
render_lines.push(Line::default());
}
let mut paragraph = Paragraph::new(render_lines).style(base_style);
// If wrapping is enabled, we need to manually wrap the lines
// For now, we'll convert to plain text, wrap, and lose styling
// This ensures consistency with cursor calculation
if wrap_lines {
paragraph = paragraph.wrap(Wrap { trim: false });
let content_width = inner.width as usize;
let mut wrapped_lines: Vec<Line> = Vec::new();
for (row_idx, line) in render_lines.iter().enumerate() {
let line_text = line.to_string();
let segments = wrap_line_segments(&line_text, content_width);
for (seg_idx, segment) in segments.into_iter().enumerate() {
// For the line with the cursor, preserve the cursor line style
if row_idx == cursor.0 && seg_idx == 0 {
wrapped_lines.push(Line::from(segment).patch_style(cursor_line_style));
} else {
wrapped_lines.push(Line::from(segment));
}
}
}
render_lines = wrapped_lines;
}
let mut paragraph = Paragraph::new(render_lines).style(base_style);
let metrics = compute_cursor_metrics(lines_slice, cursor, mask_char, inner, wrap_lines);
@@ -307,15 +327,48 @@ fn wrap_line_segments(line: &str, width: usize) -> Vec<String> {
return vec![String::new()];
}
let wrapped = wrap(line, Options::new(width).break_words(false));
if wrapped.is_empty() {
vec![String::new()]
} else {
wrapped
.into_iter()
.map(|segment| segment.into_owned())
.collect()
if line.is_empty() {
return vec![String::new()];
}
// Manual wrapping that preserves all characters including spaces
let mut result = Vec::new();
let mut current = String::new();
let mut current_width = 0usize;
for ch in line.chars() {
let ch_width = UnicodeWidthStr::width(ch.to_string().as_str());
// If adding this character would exceed width, wrap to next line
if current_width + ch_width > width {
if !current.is_empty() {
result.push(current);
current = String::new();
current_width = 0;
}
// If even a single character is too wide, add it anyway to avoid infinite loop
if ch_width > width {
current.push(ch);
result.push(current);
current = String::new();
current_width = 0;
continue;
}
}
current.push(ch);
current_width += ch_width;
}
if !current.is_empty() {
result.push(current);
}
if result.is_empty() {
result.push(String::new());
}
result
}
fn render_header(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
@@ -498,6 +551,7 @@ fn render_input(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
if matches!(app.mode(), InputMode::Editing) {
let mut textarea = app.textarea().clone();
textarea.set_block(input_block.clone());
textarea.set_hard_tab_indent(false);
render_editable_textarea(frame, area, &mut textarea, true);
} else {
// In non-editing mode, show the current input buffer content as read-only