This commit is contained in:
Mikhail Kilin
2026-01-22 15:26:15 +03:00
parent 1ef341d907
commit c18f43664e
10 changed files with 436 additions and 87 deletions

View File

@@ -8,14 +8,66 @@ use ratatui::{
use crate::app::App;
use crate::utils::{format_timestamp, format_date, get_day};
/// Разбивает текст на строки с учётом максимальной ширины
fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
if max_width == 0 {
return vec![text.to_string()];
}
let mut result = Vec::new();
let mut current_line = String::new();
let mut current_width = 0;
for word in text.split_whitespace() {
let word_width = word.chars().count();
if current_width == 0 {
// Первое слово в строке
current_line = word.to_string();
current_width = word_width;
} else if current_width + 1 + word_width <= max_width {
// Слово помещается
current_line.push(' ');
current_line.push_str(word);
current_width += 1 + word_width;
} else {
// Слово не помещается, начинаем новую строку
result.push(current_line);
current_line = word.to_string();
current_width = word_width;
}
}
if !current_line.is_empty() {
result.push(current_line);
}
if result.is_empty() {
result.push(String::new());
}
result
}
pub fn render(f: &mut Frame, area: Rect, app: &App) {
if let Some(chat) = app.get_selected_chat() {
// Вычисляем динамическую высоту инпута на основе длины текста
let input_width = area.width.saturating_sub(4) as usize; // -2 для рамок, -2 для "> "
let input_text_len = app.message_input.chars().count() + 2; // +2 для "> "
let input_lines = if input_width > 0 {
((input_text_len as f32 / input_width as f32).ceil() as u16).max(1)
} else {
1
};
// Минимум 3 строки (1 контент + 2 рамки), максимум 10
let input_height = (input_lines + 2).min(10).max(3);
let message_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Chat header
Constraint::Min(0), // Messages
Constraint::Length(3), // Input box
Constraint::Length(3), // Chat header
Constraint::Min(0), // Messages
Constraint::Length(input_height), // Input box (динамическая высота)
])
.split(area);
@@ -112,22 +164,62 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
// Исходящие: справа, формат "текст (HH:MM ✓✓)"
let read_mark = if msg.is_read { "✓✓" } else { "" };
let time_mark = format!("({} {})", time, read_mark);
let msg_text = format!("{} {}", msg.content, time_mark);
let msg_len = msg_text.chars().count();
let padding = content_width.saturating_sub(msg_len + 1);
let time_mark_len = time_mark.chars().count() + 1; // +1 для пробела
lines.push(Line::from(vec![
Span::raw(" ".repeat(padding)),
Span::styled(msg.content.clone(), Style::default().fg(Color::Green)),
Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)),
]));
// Максимальная ширина для текста сообщения (оставляем место для time_mark)
let max_msg_width = content_width.saturating_sub(time_mark_len + 2);
let wrapped_lines = wrap_text(&msg.content, max_msg_width);
let total_wrapped = wrapped_lines.len();
for (i, line_text) in wrapped_lines.into_iter().enumerate() {
let is_last_line = i == total_wrapped - 1;
let line_len = line_text.chars().count();
if is_last_line {
// Последняя строка — добавляем time_mark
let full_len = line_len + time_mark_len;
let padding = content_width.saturating_sub(full_len + 1);
lines.push(Line::from(vec![
Span::raw(" ".repeat(padding)),
Span::styled(line_text, Style::default().fg(Color::Green)),
Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)),
]));
} else {
// Промежуточные строки — просто текст справа
let padding = content_width.saturating_sub(line_len + 1);
lines.push(Line::from(vec![
Span::raw(" ".repeat(padding)),
Span::styled(line_text, Style::default().fg(Color::Green)),
]));
}
}
} else {
// Входящие: слева, формат "(HH:MM) текст"
let time_str = format!("({})", time);
lines.push(Line::from(vec![
Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)),
Span::raw(format!(" {}", msg.content)),
]));
let time_prefix_len = time_str.chars().count() + 2; // " (HH:MM) "
// Максимальная ширина для текста
let max_msg_width = content_width.saturating_sub(time_prefix_len + 1);
let wrapped_lines = wrap_text(&msg.content, max_msg_width);
for (i, line_text) in wrapped_lines.into_iter().enumerate() {
if i == 0 {
// Первая строка — с временем
lines.push(Line::from(vec![
Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)),
Span::raw(format!(" {}", line_text)),
]));
} else {
// Последующие строки — с отступом
let indent = " ".repeat(time_prefix_len);
lines.push(Line::from(vec![
Span::raw(indent),
Span::raw(line_text),
]));
}
}
}
}
@@ -155,7 +247,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
.scroll((scroll_offset, 0));
f.render_widget(messages_widget, message_chunks[1]);
// Input box
// Input box с wrap для длинного текста
let input_text = if app.message_input.is_empty() {
"> Введите сообщение...".to_string()
} else {
@@ -168,7 +260,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
};
let input = Paragraph::new(input_text)
.block(Block::default().borders(Borders::ALL))
.style(input_style);
.style(input_style)
.wrap(ratatui::widgets::Wrap { trim: false });
f.render_widget(input, message_chunks[2]);
} else {
let empty = Paragraph::new("Выберите чат")