/// Состояние написания сообщения /// /// Отвечает за: /// - Текст сообщения /// - Позицию курсора /// - Typing indicator use std::time::Instant; /// Состояние написания сообщения #[derive(Debug, Clone)] pub struct ComposeState { /// Текст вводимого сообщения pub message_input: String, /// Позиция курсора в message_input (в символах, не байтах) pub cursor_position: usize, /// Время последней отправки typing status (для throttling) pub last_typing_sent: Option, } impl Default for ComposeState { fn default() -> Self { Self { message_input: String::new(), cursor_position: 0, last_typing_sent: None, } } } impl ComposeState { /// Создать новое состояние написания сообщения pub fn new() -> Self { Self::default() } // === Message input === pub fn message_input(&self) -> &str { &self.message_input } pub fn message_input_mut(&mut self) -> &mut String { &mut self.message_input } pub fn set_message_input(&mut self, input: String) { self.message_input = input; self.cursor_position = self.message_input.chars().count(); } pub fn clear_message_input(&mut self) { self.message_input.clear(); self.cursor_position = 0; } pub fn is_empty(&self) -> bool { self.message_input.is_empty() } // === Cursor position === pub fn cursor_position(&self) -> usize { self.cursor_position } pub fn set_cursor_position(&mut self, pos: usize) { let max_pos = self.message_input.chars().count(); self.cursor_position = pos.min(max_pos); } pub fn move_cursor_left(&mut self) { if self.cursor_position > 0 { self.cursor_position -= 1; } } pub fn move_cursor_right(&mut self) { let max_pos = self.message_input.chars().count(); if self.cursor_position < max_pos { self.cursor_position += 1; } } pub fn move_cursor_to_start(&mut self) { self.cursor_position = 0; } pub fn move_cursor_to_end(&mut self) { self.cursor_position = self.message_input.chars().count(); } // === Typing indicator === pub fn last_typing_sent(&self) -> Option { self.last_typing_sent } pub fn set_last_typing_sent(&mut self, time: Option) { self.last_typing_sent = time; } pub fn update_last_typing_sent(&mut self) { self.last_typing_sent = Some(Instant::now()); } pub fn clear_typing_indicator(&mut self) { self.last_typing_sent = None; } /// Проверить, нужно ли отправить typing indicator /// (если прошло больше 5 секунд с последней отправки) pub fn should_send_typing(&self) -> bool { match self.last_typing_sent { None => true, Some(last) => last.elapsed().as_secs() >= 5, } } // === Text editing === /// Вставить символ в текущую позицию курсора pub fn insert_char(&mut self, c: char) { let char_indices: Vec = self.message_input.char_indices().map(|(i, _)| i).collect(); let byte_pos = if self.cursor_position >= char_indices.len() { self.message_input.len() } else { char_indices[self.cursor_position] }; self.message_input.insert(byte_pos, c); self.cursor_position += 1; } /// Удалить символ перед курсором (Backspace) pub fn delete_char_before_cursor(&mut self) { if self.cursor_position > 0 { let char_indices: Vec = self.message_input.char_indices().map(|(i, _)| i).collect(); let byte_pos = char_indices[self.cursor_position - 1]; self.message_input.remove(byte_pos); self.cursor_position -= 1; } } /// Удалить символ после курсора (Delete) pub fn delete_char_after_cursor(&mut self) { let char_indices: Vec = self.message_input.char_indices().map(|(i, _)| i).collect(); if self.cursor_position < char_indices.len() { let byte_pos = char_indices[self.cursor_position]; self.message_input.remove(byte_pos); } } /// Удалить слово перед курсором (Ctrl+Backspace) pub fn delete_word_before_cursor(&mut self) { if self.cursor_position == 0 { return; } let chars: Vec = self.message_input.chars().collect(); let mut pos = self.cursor_position; // Пропустить пробелы while pos > 0 && chars[pos - 1].is_whitespace() { pos -= 1; } // Удалить символы слова while pos > 0 && !chars[pos - 1].is_whitespace() { pos -= 1; } let removed_count = self.cursor_position - pos; if removed_count > 0 { let char_indices: Vec = self.message_input.char_indices().map(|(i, _)| i).collect(); let start_byte = char_indices[pos]; let end_byte = if self.cursor_position >= char_indices.len() { self.message_input.len() } else { char_indices[self.cursor_position] }; self.message_input.drain(start_byte..end_byte); self.cursor_position = pos; } } /// Очистить всё и сбросить состояние pub fn reset(&mut self) { self.message_input.clear(); self.cursor_position = 0; self.last_typing_sent = None; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_insert_char() { let mut state = ComposeState::new(); state.insert_char('H'); state.insert_char('i'); assert_eq!(state.message_input(), "Hi"); assert_eq!(state.cursor_position(), 2); } #[test] fn test_delete_char_before_cursor() { let mut state = ComposeState::new(); state.set_message_input("Hello".to_string()); state.delete_char_before_cursor(); assert_eq!(state.message_input(), "Hell"); assert_eq!(state.cursor_position(), 4); } #[test] fn test_cursor_movement() { let mut state = ComposeState::new(); state.set_message_input("Hello".to_string()); state.move_cursor_to_start(); assert_eq!(state.cursor_position(), 0); state.move_cursor_right(); assert_eq!(state.cursor_position(), 1); state.move_cursor_to_end(); assert_eq!(state.cursor_position(), 5); state.move_cursor_left(); assert_eq!(state.cursor_position(), 4); } #[test] fn test_delete_word() { let mut state = ComposeState::new(); state.set_message_input("Hello World".to_string()); state.delete_word_before_cursor(); assert_eq!(state.message_input(), "Hello "); } }