fixes
This commit is contained in:
@@ -24,19 +24,40 @@ struct WrappedLine {
|
||||
start_offset: usize,
|
||||
}
|
||||
|
||||
/// Разбивает текст на строки с учётом максимальной ширины
|
||||
/// Разбивает текст на строки с учётом максимальной ширины и `\n`
|
||||
fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
let mut all_lines = Vec::new();
|
||||
let mut char_offset = 0;
|
||||
|
||||
for segment in text.split('\n') {
|
||||
let wrapped = wrap_paragraph(segment, max_width, char_offset);
|
||||
all_lines.extend(wrapped);
|
||||
char_offset += segment.chars().count() + 1; // +1 за '\n'
|
||||
}
|
||||
|
||||
if all_lines.is_empty() {
|
||||
all_lines.push(WrappedLine {
|
||||
text: String::new(),
|
||||
start_offset: 0,
|
||||
});
|
||||
}
|
||||
|
||||
all_lines
|
||||
}
|
||||
|
||||
/// Разбивает один абзац (без `\n`) на строки по ширине
|
||||
fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec<WrappedLine> {
|
||||
if max_width == 0 {
|
||||
return vec![WrappedLine {
|
||||
text: text.to_string(),
|
||||
start_offset: 0,
|
||||
start_offset: base_offset,
|
||||
}];
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut current_line = String::new();
|
||||
let mut current_width = 0;
|
||||
let mut line_start_offset = 0;
|
||||
let mut line_start_offset = base_offset;
|
||||
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut word_start = 0;
|
||||
@@ -51,7 +72,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
if current_width == 0 {
|
||||
current_line = word;
|
||||
current_width = word_width;
|
||||
line_start_offset = word_start;
|
||||
line_start_offset = base_offset + word_start;
|
||||
} else if current_width + 1 + word_width <= max_width {
|
||||
current_line.push(' ');
|
||||
current_line.push_str(&word);
|
||||
@@ -63,7 +84,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
});
|
||||
current_line = word;
|
||||
current_width = word_width;
|
||||
line_start_offset = word_start;
|
||||
line_start_offset = base_offset + word_start;
|
||||
}
|
||||
in_word = false;
|
||||
}
|
||||
@@ -79,7 +100,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
|
||||
if current_width == 0 {
|
||||
current_line = word;
|
||||
line_start_offset = word_start;
|
||||
line_start_offset = base_offset + word_start;
|
||||
} else if current_width + 1 + word_width <= max_width {
|
||||
current_line.push(' ');
|
||||
current_line.push_str(&word);
|
||||
@@ -89,7 +110,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
start_offset: line_start_offset,
|
||||
});
|
||||
current_line = word;
|
||||
line_start_offset = word_start;
|
||||
line_start_offset = base_offset + word_start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +124,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
if result.is_empty() {
|
||||
result.push(WrappedLine {
|
||||
text: String::new(),
|
||||
start_offset: 0,
|
||||
start_offset: base_offset,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -288,11 +309,15 @@ pub fn render_message_bubble(
|
||||
let full_len = line_len + time_mark_len + marker_len;
|
||||
let padding = content_width.saturating_sub(full_len + 1);
|
||||
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
|
||||
if is_selected {
|
||||
if is_selected && i == 0 {
|
||||
// Одна строка — маркер на ней
|
||||
line_spans.push(Span::styled(
|
||||
selection_marker,
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else if is_selected {
|
||||
// Последняя строка multi-line — пробелы вместо маркера
|
||||
line_spans.push(Span::raw(" ".repeat(marker_len)));
|
||||
}
|
||||
line_spans.extend(formatted_spans);
|
||||
line_spans.push(Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)));
|
||||
@@ -305,6 +330,9 @@ pub fn render_message_bubble(
|
||||
selection_marker,
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else if is_selected {
|
||||
// Средние строки multi-line — пробелы вместо маркера
|
||||
line_spans.push(Span::raw(" ".repeat(marker_len)));
|
||||
}
|
||||
line_spans.extend(formatted_spans);
|
||||
lines.push(Line::from(line_spans));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Compose bar / input box rendering
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::InputMode;
|
||||
use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods};
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::ui::components;
|
||||
@@ -19,13 +20,12 @@ fn render_input_with_cursor(
|
||||
cursor_pos: usize,
|
||||
color: Color,
|
||||
) -> Line<'static> {
|
||||
// Используем компонент input_field
|
||||
components::render_input_field(prefix, text, cursor_pos, color)
|
||||
}
|
||||
|
||||
/// Renders input box with support for different modes (forward/select/edit/reply/normal)
|
||||
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
let (input_line, input_title) = if app.is_forwarding() {
|
||||
let (input_line, input_title): (Line, &str) = if app.is_forwarding() {
|
||||
// Режим пересылки - показываем превью сообщения
|
||||
let forward_preview = app
|
||||
.get_forwarding_message()
|
||||
@@ -67,7 +67,6 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
} else if app.is_editing() {
|
||||
// Режим редактирования
|
||||
if app.message_input.is_empty() {
|
||||
// Пустой инпут - показываем курсор и placeholder
|
||||
let line = Line::from(vec![
|
||||
Span::raw("✏ "),
|
||||
Span::styled("█", Style::default().fg(Color::Magenta)),
|
||||
@@ -75,7 +74,6 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
]);
|
||||
(line, " Редактирование (Esc отмена) ")
|
||||
} else {
|
||||
// Текст с курсором
|
||||
let line = render_input_with_cursor(
|
||||
"✏ ",
|
||||
&app.message_input,
|
||||
@@ -123,10 +121,25 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
);
|
||||
(line, " Ответ (Esc отмена) ")
|
||||
}
|
||||
} else {
|
||||
// Обычный режим
|
||||
} else if app.input_mode == InputMode::Normal {
|
||||
// Normal mode — dim, no cursor
|
||||
if app.message_input.is_empty() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("> Press i to type...", Style::default().fg(Color::DarkGray)),
|
||||
]);
|
||||
(line, "")
|
||||
} else {
|
||||
let draft_preview: String = app.message_input.chars().take(60).collect();
|
||||
let ellipsis = if app.message_input.chars().count() > 60 { "..." } else { "" };
|
||||
let line = Line::from(Span::styled(
|
||||
format!("> {}{}", draft_preview, ellipsis),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
(line, "")
|
||||
}
|
||||
} else {
|
||||
// Insert mode — active, with cursor
|
||||
if app.message_input.is_empty() {
|
||||
// Пустой инпут - показываем курсор и placeholder
|
||||
let line = Line::from(vec![
|
||||
Span::raw("> "),
|
||||
Span::styled("█", Style::default().fg(Color::Yellow)),
|
||||
@@ -134,7 +147,6 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
]);
|
||||
(line, "")
|
||||
} else {
|
||||
// Текст с курсором
|
||||
let line = render_input_with_cursor(
|
||||
"> ",
|
||||
&app.message_input,
|
||||
@@ -146,7 +158,12 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
};
|
||||
|
||||
let input_block = if input_title.is_empty() {
|
||||
Block::default().borders(Borders::ALL)
|
||||
let border_style = if app.input_mode == InputMode::Insert {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
Block::default().borders(Borders::ALL).border_style(border_style)
|
||||
} else {
|
||||
let title_color = if app.is_replying() || app.is_forwarding() {
|
||||
Color::Cyan
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::app::InputMode;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::tdlib::NetworkState;
|
||||
use ratatui::{
|
||||
@@ -25,7 +26,11 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
} else if app.is_searching {
|
||||
format!(" {}↑/↓: Navigate | Enter: Select | Esc: Cancel ", network_indicator)
|
||||
} else if app.selected_chat_id.is_some() {
|
||||
format!(" {}↑/↓: Scroll | Ctrl+U: Profile | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
|
||||
let mode_str = match app.input_mode {
|
||||
InputMode::Normal => "[NORMAL] j/k: Nav | i: Insert | d/r/f/y: Actions | Esc: Close",
|
||||
InputMode::Insert => "[INSERT] Type message | Esc: Normal mode",
|
||||
};
|
||||
format!(" {}{} | Ctrl+C: Quit ", network_indicator, mode_str)
|
||||
} else {
|
||||
format!(
|
||||
" {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ",
|
||||
|
||||
@@ -385,9 +385,9 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
|
||||
if let Some(chat) = app.get_selected_chat().cloned() {
|
||||
// Вычисляем динамическую высоту инпута на основе длины текста
|
||||
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)
|
||||
let input_lines: u16 = if input_width > 0 {
|
||||
let len = app.message_input.chars().count() + 2; // +2 для "> "
|
||||
((len as f32 / input_width as f32).ceil() as u16).max(1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user