refactor: extract message list and input box rendering (ui/messages.rs)
Завершена Phase 5 - полная декомпозиция render(): - render_message_list() - список сообщений с автоскроллом (~100 строк) - render_input_box() - input с режимами forward/select/edit/reply (~145 строк) Результат: - render() сокращена с ~390 до ~92 строк (76% ✂️) - 4 извлечённые функции: header, pinned, message_list, input_box - Каждая функция имеет чёткую ответственность Файл: 879 → 905 строк (+26 doc-комментариев) Phase 5 завершена: ui/messages.rs рефакторинг выполнен! 🎉 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,73 +191,9 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
// Режим профиля
|
||||
if app.is_profile_mode() {
|
||||
if let Some(profile) = app.get_profile_info() {
|
||||
crate::ui::profile::render(f, area, app, profile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Режим поиска по сообщениям
|
||||
if app.is_message_search_mode() {
|
||||
render_search_mode(f, area, app);
|
||||
return;
|
||||
}
|
||||
|
||||
// Режим просмотра закреплённых сообщений
|
||||
if app.is_pinned_mode() {
|
||||
render_pinned_mode(f, area, app);
|
||||
return;
|
||||
}
|
||||
|
||||
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 has_pinned = app.td_client.current_pinned_message().is_some();
|
||||
|
||||
let message_chunks = if has_pinned {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Length(1), // Pinned bar
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area)
|
||||
} else {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Length(0), // Pinned bar (hidden)
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area)
|
||||
};
|
||||
|
||||
// Chat header с typing status
|
||||
render_chat_header(f, message_chunks[0], app, chat);
|
||||
|
||||
// Pinned bar (если есть закреплённое сообщение)
|
||||
render_pinned_bar(f, message_chunks[1], app);
|
||||
|
||||
// Ширина области сообщений (без рамок)
|
||||
let content_width = message_chunks[2].width.saturating_sub(2) as usize;
|
||||
/// Рендерит список сообщений с группировкой по дате/отправителю и автоскроллом
|
||||
fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
let content_width = area.width.saturating_sub(2) as usize;
|
||||
|
||||
// Messages с группировкой по дате и отправителю
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
@@ -316,7 +252,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
}
|
||||
|
||||
// Вычисляем скролл с учётом пользовательского offset
|
||||
let visible_height = message_chunks[2].height.saturating_sub(2) as usize;
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
let total_lines = lines.len();
|
||||
|
||||
// Базовый скролл (показываем последние сообщения)
|
||||
@@ -350,9 +286,11 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
let messages_widget = Paragraph::new(lines)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.scroll((scroll_offset, 0));
|
||||
f.render_widget(messages_widget, message_chunks[2]);
|
||||
f.render_widget(messages_widget, area);
|
||||
}
|
||||
|
||||
// Input box с wrap для длинного текста и блочным курсором
|
||||
/// Рендерит input box с поддержкой разных режимов (forward/select/edit/reply/normal)
|
||||
fn render_input_box<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
let (input_line, input_title) = if app.is_forwarding() {
|
||||
// Режим пересылки - показываем превью сообщения
|
||||
let forward_preview = app
|
||||
@@ -494,7 +432,79 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
let input = Paragraph::new(input_line)
|
||||
.block(input_block)
|
||||
.wrap(ratatui::widgets::Wrap { trim: false });
|
||||
f.render_widget(input, message_chunks[3]);
|
||||
f.render_widget(input, area);
|
||||
}
|
||||
|
||||
|
||||
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
// Режим профиля
|
||||
if app.is_profile_mode() {
|
||||
if let Some(profile) = app.get_profile_info() {
|
||||
crate::ui::profile::render(f, area, app, profile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Режим поиска по сообщениям
|
||||
if app.is_message_search_mode() {
|
||||
render_search_mode(f, area, app);
|
||||
return;
|
||||
}
|
||||
|
||||
// Режим просмотра закреплённых сообщений
|
||||
if app.is_pinned_mode() {
|
||||
render_pinned_mode(f, area, app);
|
||||
return;
|
||||
}
|
||||
|
||||
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 has_pinned = app.td_client.current_pinned_message().is_some();
|
||||
|
||||
let message_chunks = if has_pinned {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Length(1), // Pinned bar
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area)
|
||||
} else {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Length(0), // Pinned bar (hidden)
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area)
|
||||
};
|
||||
|
||||
// Chat header с typing status
|
||||
render_chat_header(f, message_chunks[0], app, chat);
|
||||
|
||||
// Pinned bar (если есть закреплённое сообщение)
|
||||
render_pinned_bar(f, message_chunks[1], app);
|
||||
|
||||
// Messages с группировкой по дате и отправителю
|
||||
render_message_list(f, message_chunks[2], app);
|
||||
|
||||
// Input box с wrap для длинного текста и блочным курсором
|
||||
render_input_box(f, message_chunks[3], app);
|
||||
} else {
|
||||
let empty = Paragraph::new("Выберите чат")
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
|
||||
Reference in New Issue
Block a user