mod cursor; mod layout; mod tree_blocks; mod widgets; use std::sync::Arc; use async_trait::async_trait; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use parking_lot::FairMutex; use tokio::sync::Mutex; use toss::frame::{Frame, Pos, Size}; use toss::terminal::Terminal; use crate::store::{Msg, MsgStore}; use crate::ui::widgets::editor::EditorState; use crate::ui::widgets::Widget; use self::cursor::Cursor; use super::{ChatMsg, Reaction}; /////////// // State // /////////// enum Correction { MakeCursorVisible, MoveCursorToVisibleArea, } struct InnerTreeViewState> { store: S, last_cursor: Cursor, last_cursor_line: i32, cursor: Cursor, /// Scroll the view on the next render. Positive values scroll up and /// negative values scroll down. scroll: i32, correction: Option, editor: EditorState, } impl> InnerTreeViewState { fn new(store: S) -> Self { Self { store, last_cursor: Cursor::Bottom, last_cursor_line: 0, cursor: Cursor::Bottom, scroll: 0, correction: None, editor: EditorState::new(), } } fn handle_editor_key_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: KeyEvent, coming_from: Option, parent: Option, ) -> Reaction { let harmless_char = event.modifiers.difference(KeyModifiers::SHIFT).is_empty(); match event.code { KeyCode::Esc => { self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom); Reaction::Handled } KeyCode::Enter => { let content = self.editor.text(); if content.trim().is_empty() { Reaction::Handled } else { self.cursor = Cursor::Pseudo { coming_from, parent: parent.clone(), }; Reaction::Composed { parent, content } } } KeyCode::Backspace => { self.editor.backspace(); self.correction = Some(Correction::MakeCursorVisible); Reaction::Handled } KeyCode::Left => { self.editor.move_cursor_left(); self.correction = Some(Correction::MakeCursorVisible); Reaction::Handled } KeyCode::Right => { self.editor.move_cursor_right(); self.correction = Some(Correction::MakeCursorVisible); Reaction::Handled } KeyCode::Delete => { self.editor.delete(); self.correction = Some(Correction::MakeCursorVisible); Reaction::Handled } KeyCode::Char(ch) if harmless_char => { self.editor.insert_char(ch); self.correction = Some(Correction::MakeCursorVisible); Reaction::Handled } KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => { self.editor.edit_externally(terminal, crossterm_lock); self.correction = Some(Correction::MakeCursorVisible); Reaction::Handled } _ => Reaction::NotHandled, } } async fn handle_movement_key_event(&mut self, frame: &mut Frame, event: KeyEvent) -> bool { let chat_height = frame.size().height - 3; let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty(); match event.code { KeyCode::Char('k') | KeyCode::Up if shift_only => self.move_cursor_up().await, KeyCode::Char('j') | KeyCode::Down if shift_only => self.move_cursor_down().await, KeyCode::Char('g') | KeyCode::Home if shift_only => self.move_cursor_to_top().await, KeyCode::Char('G') | KeyCode::End if shift_only => self.move_cursor_to_bottom().await, KeyCode::Char('y') if event.modifiers == KeyModifiers::CONTROL => self.scroll_up(1), KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => self.scroll_down(1), KeyCode::Char('u') if event.modifiers == KeyModifiers::CONTROL => { let delta = chat_height / 2; self.scroll_up(delta.into()); } KeyCode::Char('d') if event.modifiers == KeyModifiers::CONTROL => { let delta = chat_height / 2; self.scroll_down(delta.into()); } KeyCode::Char('b') if event.modifiers == KeyModifiers::CONTROL => { let delta = chat_height.saturating_sub(1); self.scroll_up(delta.into()); } KeyCode::Char('f') if event.modifiers == KeyModifiers::CONTROL => { let delta = chat_height.saturating_sub(1); self.scroll_down(delta.into()); } _ => return false, } true } async fn handle_edit_initiating_key_event( &mut self, event: KeyEvent, id: Option, ) -> bool { let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty(); if !shift_only { return false; } match event.code { KeyCode::Char('r') => { if let Some(parent) = self.parent_for_normal_reply().await { self.cursor = Cursor::Editor { coming_from: id, parent, }; self.correction = Some(Correction::MakeCursorVisible); } } KeyCode::Char('R') => { if let Some(parent) = self.parent_for_alternate_reply().await { self.cursor = Cursor::Editor { coming_from: id, parent, }; self.correction = Some(Correction::MakeCursorVisible); } } KeyCode::Char('t' | 'T') => { self.cursor = Cursor::Editor { coming_from: id, parent: None, }; self.correction = Some(Correction::MakeCursorVisible); } _ => return false, } true } async fn handle_normal_key_event( &mut self, frame: &mut Frame, event: KeyEvent, can_compose: bool, id: Option, ) -> bool { if self.handle_movement_key_event(frame, event).await { true } else if can_compose { self.handle_edit_initiating_key_event(event, id).await } else { false } } async fn handle_key_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: KeyEvent, can_compose: bool, ) -> Reaction { match &self.cursor { Cursor::Bottom => { if self .handle_normal_key_event(terminal.frame(), event, can_compose, None) .await { Reaction::Handled } else { Reaction::NotHandled } } Cursor::Msg(id) => { let id = id.clone(); if self .handle_normal_key_event(terminal.frame(), event, can_compose, Some(id)) .await { Reaction::Handled } else { Reaction::NotHandled } } Cursor::Editor { coming_from, parent, } => self.handle_editor_key_event( terminal, crossterm_lock, event, coming_from.clone(), parent.clone(), ), Cursor::Pseudo { .. } => { if self .handle_movement_key_event(terminal.frame(), event) .await { Reaction::Handled } else { Reaction::NotHandled } } } } fn sent(&mut self, id: Option) { if let Cursor::Pseudo { coming_from, .. } = &self.cursor { if let Some(id) = id { self.cursor = Cursor::Msg(id); self.editor.clear(); } else { self.cursor = match coming_from { Some(id) => Cursor::Msg(id.clone()), None => Cursor::Bottom, }; }; } } } pub struct TreeViewState>(Arc>>); impl> TreeViewState { pub fn new(store: S) -> Self { Self(Arc::new(Mutex::new(InnerTreeViewState::new(store)))) } pub fn widget(&self, nick: String) -> TreeView { TreeView { inner: self.0.clone(), nick, } } pub async fn handle_key_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: KeyEvent, can_compose: bool, ) -> Reaction { self.0 .lock() .await .handle_key_event(terminal, crossterm_lock, event, can_compose) .await } pub async fn sent(&mut self, id: Option) { self.0.lock().await.sent(id) } } //////////// // Widget // //////////// pub struct TreeView> { inner: Arc>>, nick: String, } #[async_trait] impl Widget for TreeView where M: Msg + ChatMsg, M::Id: Send + Sync, S: MsgStore + Send + Sync, { fn size(&self, _frame: &mut Frame, _max_width: Option, _max_height: Option) -> Size { Size::ZERO } async fn render(self: Box, frame: &mut Frame) { let mut guard = self.inner.lock().await; let blocks = guard.relayout(&self.nick, frame).await; let size = frame.size(); for block in blocks.into_blocks().blocks { frame.push( Pos::new(0, block.top_line), Size::new(size.width, block.height as u16), ); block.widget.render(frame).await; frame.pop(); } } }