Refactor TUI line wrapping: implement manual wrapping for consistency, preserve cursor styles, and handle wide characters.
This commit is contained in:
@@ -101,12 +101,32 @@ 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);
|
||||
|
||||
if let Some(ref metrics) = metrics {
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user