fixes
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use crate::app::App;
|
||||
use crate::formatting;
|
||||
use crate::ui::components;
|
||||
use crate::utils::{format_date, format_timestamp_with_tz, get_day};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
@@ -7,188 +9,15 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use tdlib_rs::enums::TextEntityType;
|
||||
use tdlib_rs::types::TextEntity;
|
||||
|
||||
/// Структура для хранения стиля символа
|
||||
#[derive(Clone, Default)]
|
||||
struct CharStyle {
|
||||
bold: bool,
|
||||
italic: bool,
|
||||
underline: bool,
|
||||
strikethrough: bool,
|
||||
code: bool,
|
||||
spoiler: bool,
|
||||
url: bool,
|
||||
mention: bool,
|
||||
}
|
||||
|
||||
impl CharStyle {
|
||||
fn to_style(&self, base_color: Color) -> Style {
|
||||
let mut style = Style::default();
|
||||
|
||||
if self.code {
|
||||
// Код отображается cyan на тёмном фоне
|
||||
style = style.fg(Color::Cyan).bg(Color::DarkGray);
|
||||
} else if self.spoiler {
|
||||
// Спойлер — серый текст (скрытый)
|
||||
style = style.fg(Color::DarkGray).bg(Color::DarkGray);
|
||||
} else if self.url || self.mention {
|
||||
// Ссылки и упоминания — синий с подчёркиванием
|
||||
style = style.fg(Color::Blue).add_modifier(Modifier::UNDERLINED);
|
||||
} else {
|
||||
style = style.fg(base_color);
|
||||
}
|
||||
|
||||
if self.bold {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if self.italic {
|
||||
style = style.add_modifier(Modifier::ITALIC);
|
||||
}
|
||||
if self.underline {
|
||||
style = style.add_modifier(Modifier::UNDERLINED);
|
||||
}
|
||||
if self.strikethrough {
|
||||
style = style.add_modifier(Modifier::CROSSED_OUT);
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
/// Преобразует текст с entities в вектор стилизованных Span (owned)
|
||||
fn format_text_with_entities(
|
||||
text: &str,
|
||||
entities: &[TextEntity],
|
||||
base_color: Color,
|
||||
) -> Vec<Span<'static>> {
|
||||
if entities.is_empty() {
|
||||
return vec![Span::styled(
|
||||
text.to_string(),
|
||||
Style::default().fg(base_color),
|
||||
)];
|
||||
}
|
||||
|
||||
// Создаём массив стилей для каждого символа
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut char_styles: Vec<CharStyle> = vec![CharStyle::default(); chars.len()];
|
||||
|
||||
// Применяем entities к символам
|
||||
for entity in entities {
|
||||
let start = entity.offset as usize;
|
||||
let end = (entity.offset + entity.length) as usize;
|
||||
|
||||
for i in start..end.min(chars.len()) {
|
||||
match &entity.r#type {
|
||||
TextEntityType::Bold => char_styles[i].bold = true,
|
||||
TextEntityType::Italic => char_styles[i].italic = true,
|
||||
TextEntityType::Underline => char_styles[i].underline = true,
|
||||
TextEntityType::Strikethrough => char_styles[i].strikethrough = true,
|
||||
TextEntityType::Code | TextEntityType::Pre | TextEntityType::PreCode(_) => {
|
||||
char_styles[i].code = true
|
||||
}
|
||||
TextEntityType::Spoiler => char_styles[i].spoiler = true,
|
||||
TextEntityType::Url
|
||||
| TextEntityType::TextUrl(_)
|
||||
| TextEntityType::EmailAddress
|
||||
| TextEntityType::PhoneNumber => char_styles[i].url = true,
|
||||
TextEntityType::Mention | TextEntityType::MentionName(_) => {
|
||||
char_styles[i].mention = true
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Группируем последовательные символы с одинаковым стилем
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
let mut current_text = String::new();
|
||||
let mut current_style: Option<CharStyle> = None;
|
||||
|
||||
for (i, ch) in chars.iter().enumerate() {
|
||||
let style = &char_styles[i];
|
||||
|
||||
match ¤t_style {
|
||||
Some(prev_style) if styles_equal(prev_style, style) => {
|
||||
current_text.push(*ch);
|
||||
}
|
||||
_ => {
|
||||
if !current_text.is_empty() {
|
||||
if let Some(prev_style) = ¤t_style {
|
||||
spans.push(Span::styled(
|
||||
current_text.clone(),
|
||||
prev_style.to_style(base_color),
|
||||
));
|
||||
}
|
||||
}
|
||||
current_text = ch.to_string();
|
||||
current_style = Some(style.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем последний span
|
||||
if !current_text.is_empty() {
|
||||
if let Some(style) = current_style {
|
||||
spans.push(Span::styled(current_text, style.to_style(base_color)));
|
||||
}
|
||||
}
|
||||
|
||||
if spans.is_empty() {
|
||||
spans.push(Span::styled(text.to_string(), Style::default().fg(base_color)));
|
||||
}
|
||||
|
||||
spans
|
||||
}
|
||||
|
||||
/// Проверяет равенство двух стилей
|
||||
fn styles_equal(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
a.bold == b.bold
|
||||
&& a.italic == b.italic
|
||||
&& a.underline == b.underline
|
||||
&& a.strikethrough == b.strikethrough
|
||||
&& a.code == b.code
|
||||
&& a.spoiler == b.spoiler
|
||||
&& a.url == b.url
|
||||
&& a.mention == b.mention
|
||||
}
|
||||
|
||||
/// Рендерит текст инпута с блочным курсором
|
||||
fn render_input_with_cursor(
|
||||
prefix: &str,
|
||||
text: &str,
|
||||
cursor_pos: usize,
|
||||
color: Color,
|
||||
) -> Line<'static> {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())];
|
||||
|
||||
// Ограничиваем cursor_pos границами текста
|
||||
let safe_cursor_pos = cursor_pos.min(chars.len());
|
||||
|
||||
// Текст до курсора
|
||||
if safe_cursor_pos > 0 {
|
||||
let before: String = chars[..safe_cursor_pos].iter().collect();
|
||||
spans.push(Span::styled(before, Style::default().fg(color)));
|
||||
}
|
||||
|
||||
// Символ под курсором (или █ если курсор в конце)
|
||||
if safe_cursor_pos < chars.len() {
|
||||
let cursor_char = chars[safe_cursor_pos].to_string();
|
||||
spans.push(Span::styled(cursor_char, Style::default().fg(Color::Black).bg(color)));
|
||||
} else {
|
||||
// Курсор в конце - показываем блок
|
||||
spans.push(Span::styled("█", Style::default().fg(color)));
|
||||
}
|
||||
|
||||
// Текст после курсора
|
||||
if safe_cursor_pos + 1 < chars.len() {
|
||||
let after: String = chars[safe_cursor_pos + 1..].iter().collect();
|
||||
spans.push(Span::styled(after, Style::default().fg(color)));
|
||||
}
|
||||
|
||||
Line::from(spans)
|
||||
// Используем компонент input_field
|
||||
components::render_input_field(prefix, text, cursor_pos, color)
|
||||
}
|
||||
|
||||
/// Информация о строке после переноса: текст и позиция в оригинале
|
||||
@@ -282,43 +111,6 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
result
|
||||
}
|
||||
|
||||
/// Фильтрует и корректирует entities для подстроки
|
||||
fn adjust_entities_for_substring(
|
||||
entities: &[TextEntity],
|
||||
start: usize,
|
||||
length: usize,
|
||||
) -> Vec<TextEntity> {
|
||||
let start = start as i32;
|
||||
let end = start + length as i32;
|
||||
|
||||
entities
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
let e_start = e.offset;
|
||||
let e_end = e.offset + e.length;
|
||||
|
||||
// Проверяем пересечение с нашей подстрокой
|
||||
if e_end <= start || e_start >= end {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Вычисляем пересечение
|
||||
let new_start = (e_start - start).max(0);
|
||||
let new_end = (e_end - start).min(length as i32);
|
||||
|
||||
if new_end > new_start {
|
||||
Some(TextEntity {
|
||||
offset: new_start,
|
||||
length: new_end - new_start,
|
||||
r#type: e.r#type.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Режим профиля
|
||||
if app.is_profile_mode() {
|
||||
@@ -615,7 +407,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
let line_len = wrapped.text.chars().count();
|
||||
|
||||
// Получаем entities для этой строки
|
||||
let line_entities = adjust_entities_for_substring(
|
||||
let line_entities = formatting::adjust_entities_for_substring(
|
||||
msg.entities(),
|
||||
wrapped.start_offset,
|
||||
line_len,
|
||||
@@ -623,7 +415,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Форматируем текст с entities
|
||||
let formatted_spans =
|
||||
format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
|
||||
if is_last_line {
|
||||
// Последняя строка — добавляем time_mark
|
||||
@@ -675,7 +467,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
let line_len = wrapped.text.chars().count();
|
||||
|
||||
// Получаем entities для этой строки
|
||||
let line_entities = adjust_entities_for_substring(
|
||||
let line_entities = formatting::adjust_entities_for_substring(
|
||||
msg.entities(),
|
||||
wrapped.start_offset,
|
||||
line_len,
|
||||
@@ -683,7 +475,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Форматируем текст с entities
|
||||
let formatted_spans =
|
||||
format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
|
||||
if i == 0 {
|
||||
// Первая строка — с временем и маркером выбора
|
||||
@@ -717,7 +509,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
if !msg.reactions().is_empty() {
|
||||
let mut reaction_spans = vec![];
|
||||
|
||||
for reaction in &msg.reactions() {
|
||||
for reaction in msg.reactions() {
|
||||
if !reaction_spans.is_empty() {
|
||||
reaction_spans.push(Span::raw(" "));
|
||||
}
|
||||
@@ -831,10 +623,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Режим выбора сообщения - подсказка зависит от возможностей
|
||||
let selected_msg = app.get_selected_message();
|
||||
let can_edit = selected_msg
|
||||
.map(|m| m.can_be_edited && m.is_outgoing)
|
||||
.map(|m| m.can_be_edited() && m.is_outgoing())
|
||||
.unwrap_or(false);
|
||||
let can_delete = selected_msg
|
||||
.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users)
|
||||
.map(|m| m.can_be_deleted_only_for_self() || m.can_be_deleted_for_all_users())
|
||||
.unwrap_or(false);
|
||||
|
||||
let hint = match (can_edit, can_delete) {
|
||||
@@ -872,7 +664,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
let reply_preview = app
|
||||
.get_replying_to_message()
|
||||
.map(|m| {
|
||||
let sender = if m.is_outgoing {
|
||||
let sender = if m.is_outgoing() {
|
||||
"Вы"
|
||||
} else {
|
||||
m.sender_name()
|
||||
@@ -1336,56 +1128,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
/// Рендерит модалку подтверждения удаления
|
||||
fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
||||
use ratatui::widgets::Clear;
|
||||
|
||||
// Размеры модалки
|
||||
let modal_width = 40u16;
|
||||
let modal_height = 7u16;
|
||||
|
||||
// Центрируем модалку
|
||||
let x = area.x + (area.width.saturating_sub(modal_width)) / 2;
|
||||
let y = area.y + (area.height.saturating_sub(modal_height)) / 2;
|
||||
|
||||
let modal_area = Rect::new(x, y, modal_width.min(area.width), modal_height.min(area.height));
|
||||
|
||||
// Очищаем область под модалкой
|
||||
f.render_widget(Clear, modal_area);
|
||||
|
||||
// Содержимое модалки
|
||||
let text = vec![
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
"Удалить сообщение?",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
" [y/Enter] ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Да"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw("Нет"),
|
||||
]),
|
||||
];
|
||||
|
||||
let modal = Paragraph::new(text)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Red))
|
||||
.title(" Подтверждение ")
|
||||
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
f.render_widget(modal, modal_area);
|
||||
components::modal::render_delete_confirm_modal(f, area);
|
||||
}
|
||||
|
||||
/// Рендерит модалку выбора реакции
|
||||
@@ -1395,88 +1138,5 @@ fn render_reaction_picker_modal(
|
||||
available_reactions: &[String],
|
||||
selected_index: usize,
|
||||
) {
|
||||
use ratatui::widgets::Clear;
|
||||
|
||||
// Размеры модалки (зависят от количества реакций)
|
||||
let emojis_per_row = 8;
|
||||
let rows = (available_reactions.len() + emojis_per_row - 1) / emojis_per_row;
|
||||
let modal_width = 50u16;
|
||||
let modal_height = (rows + 4) as u16; // +4 для заголовка, отступов и подсказки
|
||||
|
||||
// Центрируем модалку
|
||||
let x = area.x + (area.width.saturating_sub(modal_width)) / 2;
|
||||
let y = area.y + (area.height.saturating_sub(modal_height)) / 2;
|
||||
|
||||
let modal_area = Rect::new(x, y, modal_width.min(area.width), modal_height.min(area.height));
|
||||
|
||||
// Очищаем область под модалкой
|
||||
f.render_widget(Clear, modal_area);
|
||||
|
||||
// Формируем содержимое - сетка эмодзи
|
||||
let mut text_lines = vec![Line::from("")]; // Пустая строка сверху
|
||||
|
||||
for row in 0..rows {
|
||||
let mut row_spans = vec![Span::raw(" ")]; // Отступ слева
|
||||
|
||||
for col in 0..emojis_per_row {
|
||||
let idx = row * emojis_per_row + col;
|
||||
if idx >= available_reactions.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let emoji = &available_reactions[idx];
|
||||
let is_selected = idx == selected_index;
|
||||
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
|
||||
row_spans.push(Span::styled(format!(" {} ", emoji), style));
|
||||
row_spans.push(Span::raw(" ")); // Пробел между эмодзи
|
||||
}
|
||||
|
||||
text_lines.push(Line::from(row_spans));
|
||||
}
|
||||
|
||||
// Добавляем пустую строку и подсказку
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
" [←/→/↑/↓] ",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Выбор "),
|
||||
Span::styled(
|
||||
" [Enter] ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Добавить "),
|
||||
Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw("Отмена"),
|
||||
]));
|
||||
|
||||
let modal = Paragraph::new(text_lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title(" Выбери реакцию ")
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
)
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
f.render_widget(modal, modal_area);
|
||||
components::render_emoji_picker(f, area, available_reactions, selected_index);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user