diff --git a/src/ui.rs b/src/ui.rs index a22ea89..5f6839f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -20,8 +20,9 @@ use toss::terminal::Terminal; use crate::logger::{LogMsg, Logger}; use crate::vault::Vault; -use self::chat::Chat; +use self::chat::{Chat, ChatState}; use self::rooms::Rooms; +use self::widgets::Widget; #[derive(Debug)] pub enum UiEvent { @@ -45,7 +46,7 @@ pub struct Ui { mode: Mode, rooms: Rooms, - log_chat: Chat, + log_chat: ChatState, } impl Ui { @@ -80,7 +81,7 @@ impl Ui { event_tx: event_tx.clone(), mode: Mode::Main, rooms: Rooms::new(vault, event_tx.clone()), - log_chat: Chat::new(logger), + log_chat: ChatState::new(logger), }; tokio::select! { e = ui.run_main(terminal, event_rx, crossterm_lock) => Ok(e), @@ -166,11 +167,7 @@ impl Ui { async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> { match self.mode { Mode::Main => self.rooms.render(frame).await, - Mode::Log => { - self.log_chat - .render(frame, Pos::new(0, 0), frame.size()) - .await - } + Mode::Log => Box::new(self.log_chat.widget()).render(frame).await, } Ok(()) } @@ -202,7 +199,10 @@ impl Ui { .handle_key_event(terminal, size, crossterm_lock, event) .await } - Mode::Log => self.log_chat.handle_navigation(terminal, size, event).await, + Mode::Log => { + // TODO Uncomment + // self.log_chat.handle_navigation(terminal, size, event).await + } } EventHandleResult::Continue diff --git a/src/ui/chat.rs b/src/ui/chat.rs index b695cb4..36b9b3c 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -1,15 +1,17 @@ mod tree; -use std::sync::Arc; - -use crossterm::event::KeyEvent; -use parking_lot::FairMutex; -use toss::frame::{Frame, Pos, Size}; -use toss::terminal::Terminal; +use async_trait::async_trait; +use toss::frame::{Frame, Size}; use crate::store::{Msg, MsgStore}; -use self::tree::TreeView; +use self::tree::{TreeView, TreeViewState}; + +use super::widgets::Widget; + +/////////// +// State // +/////////// pub enum Mode { Tree, @@ -34,74 +36,108 @@ impl Cursor { } } -pub struct Chat> { +pub struct ChatState> { store: S, - cursor: Option>, mode: Mode, - tree: TreeView, + tree: TreeViewState, // thread: ThreadView, // flat: FlatView, } -impl> Chat { +impl + Clone> ChatState { pub fn new(store: S) -> Self { Self { - store, - cursor: None, mode: Mode::Tree, - tree: TreeView::new(), + tree: TreeViewState::new(store.clone()), + store, } } +} +impl> ChatState { pub fn store(&self) -> &S { &self.store } + + pub fn widget(&self) -> Chat { + match self.mode { + Mode::Tree => Chat::Tree(self.tree.widget()), + } + } } impl> Chat { - pub async fn handle_navigation( - &mut self, - terminal: &mut Terminal, - size: Size, - event: KeyEvent, - ) { - match self.mode { - Mode::Tree => { - self.tree - .handle_navigation(&mut self.store, &mut self.cursor, terminal, size, event) - .await - } + // pub async fn handle_navigation( + // &mut self, + // terminal: &mut Terminal, + // size: Size, + // event: KeyEvent, + // ) { + // match self.mode { + // Mode::Tree => { + // self.tree + // .handle_navigation(&mut self.store, &mut self.cursor, terminal, size, event) + // .await + // } + // } + // } + + // pub async fn handle_messaging( + // &mut self, + // terminal: &mut Terminal, + // crossterm_lock: &Arc>, + // event: KeyEvent, + // ) -> Option<(Option, String)> { + // match self.mode { + // Mode::Tree => { + // self.tree + // .handle_messaging( + // &mut self.store, + // &mut self.cursor, + // terminal, + // crossterm_lock, + // event, + // ) + // .await + // } + // } + // } + + // pub async fn render(&mut self, frame: &mut Frame, pos: Pos, size: Size) { + // match self.mode { + // Mode::Tree => { + // self.tree + // .render(&mut self.store, &self.cursor, frame, pos, size) + // .await + // } + // } + // } +} + +//////////// +// Widget // +//////////// + +pub enum Chat> { + Tree(TreeView), +} + +#[async_trait] +impl Widget for Chat +where + M: Msg, + M::Id: Send, + S: MsgStore + Send + Sync, +{ + fn size(&self, frame: &mut Frame, max_width: Option, max_height: Option) -> Size { + match self { + Self::Tree(tree) => tree.size(frame, max_width, max_height), } } - pub async fn handle_messaging( - &mut self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - event: KeyEvent, - ) -> Option<(Option, String)> { - match self.mode { - Mode::Tree => { - self.tree - .handle_messaging( - &mut self.store, - &mut self.cursor, - terminal, - crossterm_lock, - event, - ) - .await - } - } - } - - pub async fn render(&mut self, frame: &mut Frame, pos: Pos, size: Size) { - match self.mode { - Mode::Tree => { - self.tree - .render(&mut self.store, &self.cursor, frame, pos, size) - .await - } + async fn render(self: Box, frame: &mut Frame) { + match *self { + Self::Tree(tree) => Box::new(tree).render(frame).await, } } } diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index dba8488..7525935 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -1,84 +1,126 @@ -mod action; -mod blocks; -mod cursor; -mod layout; -mod render; -mod util; +// mod action; +// mod blocks; +// mod cursor; +// mod layout; +// mod render; +// mod util; -use std::marker::PhantomData; use std::sync::Arc; -use crossterm::event::{KeyCode, KeyEvent}; -use parking_lot::FairMutex; -use toss::frame::{Frame, Pos, Size}; -use toss::terminal::Terminal; +use async_trait::async_trait; +use tokio::sync::Mutex; +use toss::frame::{Frame, Size}; use crate::store::{Msg, MsgStore}; +use crate::ui::widgets::Widget; -use super::Cursor; +/////////// +// State // +/////////// -pub struct TreeView { - // pub focus: Option, - // pub folded: HashSet, - // pub minimized: HashSet, - phantom: PhantomData, // TODO Remove +/// The anchor specifies a specific line in a room's history. +enum Anchor { + /// The bottom of the room's history stays fixed. + Bottom, + /// The top of a message stays fixed. + Msg(I), + /// The line after a message's subtree stays fixed. + After(I), } -impl TreeView { - pub fn new() -> Self { +struct Compose { + /// The message that the cursor was on when composing began, or `None` if it + /// was [`Cursor::Bottom`]. + /// + /// Used to jump back to the original position when composing is aborted + /// because the editor may be moved during composing. + coming_from: Option, + /// The parent message of this reply, or `None` if it will be a new + /// top-level message. + parent: Option, + // TODO Editor state + // TODO Whether currently editing or moving cursor +} + +struct Placeholder { + /// See [`Composing::coming_from`]. + coming_from: Option, + /// See [`Composing::parent`]. + after: Option, +} + +enum Cursor { + /// No cursor visible because it is at the bottom of the chat history. + /// + /// See also [`Anchor::Bottom`]. + Bottom, + /// The cursor points to a message. + /// + /// See also [`Anchor::Msg`]. + Msg(I), + /// The cursor has turned into an editor because we're composing a new + /// message. + /// + /// See also [`Anchor::After`]. + Compose(Compose), + /// A placeholder message is being displayed for a message that was just + /// sent by the user. + /// + /// Will be replaced by a [`Cursor::Msg`] as soon as the server replies to + /// the send command with the sent message. Otherwise, it will + /// + /// See also [`Anchor::After`]. + Placeholder(Placeholder), +} + +struct InnerTreeViewState> { + store: S, + anchor: Anchor, + anchor_line: i32, + cursor: Cursor, +} + +impl> InnerTreeViewState { + fn new(store: S) -> Self { Self { - phantom: PhantomData, + store, + anchor: Anchor::Bottom, + anchor_line: 0, + cursor: Cursor::Bottom, } } - - pub async fn handle_navigation>( - &mut self, - s: &mut S, - c: &mut Option>, - t: &mut Terminal, - z: Size, - event: KeyEvent, - ) { - match event.code { - KeyCode::Char('k') => self.move_up(s, c, t.frame(), z).await, - KeyCode::Char('j') => self.move_down(s, c, t.frame(), z).await, - KeyCode::Char('K') => self.move_up_sibling(s, c, t.frame(), z).await, - KeyCode::Char('J') => self.move_down_sibling(s, c, t.frame(), z).await, - KeyCode::Char('z') | KeyCode::Char('Z') => self.center_cursor(s, c, t.frame(), z).await, - KeyCode::Char('g') => self.move_to_first(s, c, t.frame(), z).await, - KeyCode::Char('G') => self.move_to_last(s, c, t.frame(), z).await, - KeyCode::Esc => *c = None, // TODO Make 'G' do the same thing? - _ => {} - } - } - - pub async fn handle_messaging>( - &mut self, - s: &mut S, - c: &mut Option>, - t: &mut Terminal, - l: &Arc>, - event: KeyEvent, - ) -> Option<(Option, String)> { - match event.code { - KeyCode::Char('r') => Self::reply_normal(s, c, t, l).await, - KeyCode::Char('R') => Self::reply_alternate(s, c, t, l).await, - KeyCode::Char('t') | KeyCode::Char('T') => Self::create_new_thread(t, l).await, - _ => None, - } - } - - pub async fn render>( - &mut self, - store: &mut S, - cursor: &Option>, - frame: &mut Frame, - pos: Pos, - size: Size, - ) { - let blocks = self - .layout_blocks(store, cursor.as_ref(), frame, size) - .await; - Self::render_blocks(frame, pos, size, blocks); - } +} + +pub struct TreeViewState>(Arc>>); + +impl> TreeViewState { + pub fn new(store: S) -> Self { + Self(Arc::new(Mutex::new(InnerTreeViewState::new(store)))) + } + + pub fn widget(&self) -> TreeView { + TreeView(self.0.clone()) + } +} + +//////////// +// Widget // +//////////// + +pub struct TreeView>(Arc>>); + +#[async_trait] +impl Widget for TreeView +where + M: Msg, + M::Id: Send, + 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) { + todo!() + } } diff --git a/src/ui/room.rs b/src/ui/room.rs index ee12ed1..ef632b0 100644 --- a/src/ui/room.rs +++ b/src/ui/room.rs @@ -13,7 +13,7 @@ use crate::euph::api::{SessionType, SessionView}; use crate::euph::{self, Joined, Status}; use crate::vault::{EuphMsg, EuphVault}; -use super::chat::Chat; +use super::chat::ChatState; use super::widgets::background::Background; use super::widgets::empty::Empty; use super::widgets::list::{List, ListState}; @@ -24,7 +24,7 @@ use super::{util, UiEvent}; pub struct EuphRoom { ui_event_tx: mpsc::UnboundedSender, room: Option, - chat: Chat, + chat: ChatState, nick_list_width: u16, nick_list: ListState, @@ -35,7 +35,7 @@ impl EuphRoom { Self { ui_event_tx, room: None, - chat: Chat::new(vault), + chat: ChatState::new(vault), nick_list_width: 24, nick_list: ListState::new(), } @@ -100,7 +100,9 @@ impl EuphRoom { let chat_pos = Pos::new(0, hsplit + 1); let chat_size = Size::new(size.width, size.height.saturating_sub(hsplit as u16 + 1)); - self.chat.render(frame, chat_pos, chat_size).await; + frame.push(chat_pos, chat_size); + Box::new(self.chat.widget()).render(frame).await; + frame.pop(); self.render_status(frame, status_pos, status); Self::render_hsplit(frame, hsplit); } @@ -127,7 +129,9 @@ impl EuphRoom { let nick_list_pos = Pos::new(vsplit + 1, 0); let nick_list_size = Size::new(self.nick_list_width, size.height); - self.chat.render(frame, chat_pos, chat_size).await; + frame.push(chat_pos, chat_size); + Box::new(self.chat.widget()).render(frame).await; + frame.pop(); self.render_status(frame, status_pos, status); self.render_nick_list(frame, nick_list_pos, nick_list_size, joined) .await; @@ -297,9 +301,9 @@ impl EuphRoom { let hsplit = 1_i32; let chat_size = Size::new(vsplit as u16, size.height.saturating_sub(hsplit as u16 + 1)); - self.chat - .handle_navigation(terminal, chat_size, event) - .await; + // self.chat + // .handle_navigation(terminal, chat_size, event) + // .await; if let Some(room) = &self.room { if let Ok(Some(Status::Joined(_))) = room.status().await { @@ -309,13 +313,13 @@ impl EuphRoom { } } - if let Some((parent, content)) = self - .chat - .handle_messaging(terminal, crossterm_lock, event) - .await - { - let _ = room.send(parent, content); - } + // if let Some((parent, content)) = self + // .chat + // .handle_messaging(terminal, crossterm_lock, event) + // .await + // { + // let _ = room.send(parent, content); + // } } } }