diff --git a/cove-tui/src/chat.rs b/cove-tui/src/chat.rs index 8067806..6999706 100644 --- a/cove-tui/src/chat.rs +++ b/cove-tui/src/chat.rs @@ -43,23 +43,25 @@ impl> Chat { } impl> Chat { - pub fn handle_key_event(&mut self, event: KeyEvent, size: Size) { + pub fn handle_key_event(&mut self, event: KeyEvent, frame: &mut Frame, size: Size) { match self.mode { Mode::Tree => self.tree.handle_key_event( &mut self.store, &self.room, &mut self.cursor, event, + frame, size, ), } } - pub fn render(&mut self, frame: &mut Frame, pos: Pos, size: Size) { + 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.room, &self.cursor, frame, pos, size) + .await } } } diff --git a/cove-tui/src/chat/tree.rs b/cove-tui/src/chat/tree.rs index 029f09a..3c6c653 100644 --- a/cove-tui/src/chat/tree.rs +++ b/cove-tui/src/chat/tree.rs @@ -10,14 +10,32 @@ use crate::store::{Msg, MsgStore, Tree}; use super::Cursor; +const TIME_WIDTH: usize = 5; // hh:mm +const INDENT: &str = "| "; +const INDENT_WIDTH: usize = 2; + struct Block { line: i32, height: i32, id: Option, + indent: usize, cursor: bool, content: BlockContent, } +impl Block { + fn placeholder(id: I, indent: usize) -> Self { + Self { + line: 0, + height: 1, + id: Some(id), + indent, + cursor: false, + content: BlockContent::Placeholder, + } + } +} + enum BlockContent { Msg(MsgBlock), Placeholder, @@ -25,9 +43,21 @@ enum BlockContent { struct MsgBlock { time: DateTime, - indent: usize, nick: String, - content: Vec, + lines: Vec, +} + +impl MsgBlock { + fn to_block(self, id: I, indent: usize) -> Block { + Block { + line: 0, + height: self.lines.len() as i32, + id: Some(id), + indent, + cursor: false, + content: BlockContent::Msg(self), + } + } } /// Pre-layouted messages as a sequence of blocks. @@ -108,19 +138,27 @@ impl Layout { self.bottom_line = bottom.line + bottom.height - 1; } + fn push_front(&mut self, mut block: Block) { + self.top_line -= block.height; + block.line = self.top_line; + self.blocks.push_front(block); + } + + fn push_back(&mut self, mut block: Block) { + block.line = self.bottom_line + 1; + self.bottom_line += block.height; + self.blocks.push_back(block); + } + fn prepend(&mut self, mut layout: Self) { while let Some(mut block) = layout.blocks.pop_back() { - self.top_line -= block.height; - block.line = self.top_line; - self.blocks.push_front(block); + self.push_front(block); } } fn append(&mut self, mut layout: Self) { while let Some(mut block) = layout.blocks.pop_front() { - block.line = self.bottom_line + 1; - self.bottom_line += block.height; - self.blocks.push_back(block); + self.push_back(block); } } } @@ -139,15 +177,104 @@ impl TreeView { } } - fn layout_tree(tree: Tree) -> Layout { - todo!() + fn msg_to_block( + &mut self, + msg: &M, + indent: usize, + frame: &mut Frame, + size: Size, + ) -> Block { + let nick = msg.nick(); + let content = msg.content(); + + let used_width = TIME_WIDTH + 1 + INDENT_WIDTH * indent + 1 + frame.width(&nick) + 2; + let rest_width = size.width as usize - used_width; + + let lines = toss::split_at_indices(&content, &frame.wrap(&content, rest_width)); + let lines = lines.into_iter().map(|s| s.to_string()).collect::>(); + MsgBlock { + time: msg.time(), + nick, + lines, + } + .to_block(msg.id(), indent) + } + + fn layout_subtree( + &mut self, + tree: &Tree, + frame: &mut Frame, + size: Size, + indent: usize, + id: &M::Id, + layout: &mut Layout, + ) { + let block = if let Some(msg) = tree.msg(id) { + self.msg_to_block(msg, indent, frame, size) + } else { + Block::placeholder(id.clone(), indent) + }; + layout.push_back(block); + + if let Some(children) = tree.children(id) { + for child in children { + self.layout_subtree(tree, frame, size, indent + 1, child, layout); + } + } + } + + fn layout_tree(&mut self, tree: Tree, frame: &mut Frame, size: Size) -> Layout { + let mut layout = Layout::new(); + self.layout_subtree(&tree, frame, size, 0, tree.root(), &mut layout); + layout + } + + async fn expand_layout_upwards>( + &mut self, + room: &str, + store: &S, + frame: &mut Frame, + size: Size, + layout: &mut Layout, + mut tree_id: M::Id, + ) { + while layout.top_line > 0 { + let tree = store.tree(room, &tree_id).await; + layout.prepend(self.layout_tree(tree, frame, size)); + if let Some(prev_tree_id) = store.prev_tree(room, &tree_id).await { + tree_id = prev_tree_id; + } else { + break; + } + } + } + + async fn expand_layout_downwards>( + &mut self, + room: &str, + store: &S, + frame: &mut Frame, + size: Size, + layout: &mut Layout, + mut tree_id: M::Id, + ) { + while layout.bottom_line < size.height as i32 { + let tree = store.tree(room, &tree_id).await; + layout.append(self.layout_tree(tree, frame, size)); + if let Some(next_tree_id) = store.next_tree(room, &tree_id).await { + tree_id = next_tree_id; + } else { + break; + } + } } async fn layout>( &mut self, room: &str, - store: S, + store: &S, cursor: &Option>, + frame: &mut Frame, size: Size, ) -> Layout { let height: i32 = size.height.into(); @@ -157,12 +284,23 @@ impl TreeView { // Produce layout of cursor subtree (with correct offsets) let cursor_path = store.path(room, &cursor.id).await; - let cursor_tree = store.tree(room, cursor_path.first()).await; - let mut layout = Self::layout_tree(cursor_tree); + let cursor_tree_id = cursor_path.first(); + let cursor_tree = store.tree(room, cursor_tree_id).await; + let mut layout = self.layout_tree(cursor_tree, frame, size); layout.calculate_offsets_with_cursor(cursor, height); - // TODO Expand layout upwards and downwards if there is no focus - todo!() + // Expand layout upwards and downwards + // TODO Don't do this if there is a focus + if let Some(prev_tree) = store.prev_tree(room, cursor_tree_id).await { + self.expand_layout_upwards(room, store, frame, size, &mut layout, prev_tree) + .await; + } + if let Some(next_tree) = store.next_tree(room, cursor_tree_id).await { + self.expand_layout_downwards(room, store, frame, size, &mut layout, next_tree) + .await; + } + + layout } else { // TODO Ensure there is no focus @@ -170,33 +308,72 @@ impl TreeView { let mut layout = Layout::new_below(height); // Expand layout upwards until the edge of the screen - let mut tree_id = store.last_tree(room).await; - while layout.top_line > 0 { - if let Some(actual_tree_id) = &tree_id { - let tree = store.tree(room, actual_tree_id).await; - layout.prepend(Self::layout_tree(tree)); - tree_id = store.prev_tree(room, actual_tree_id).await; - } else { - break; - } + if let Some(last_tree) = store.last_tree(room).await { + self.expand_layout_upwards(room, store, frame, size, &mut layout, last_tree) + .await; } layout } } + fn render_indentation(&mut self, frame: &mut Frame, pos: Pos, indent: usize) { + for i in 0..indent { + let x = TIME_WIDTH + 1 + INDENT_WIDTH * i; + let pos = Pos::new(pos.x + x as i32, pos.y); + frame.write(pos, INDENT, ContentStyle::default()); + } + } + + fn render_layout(&mut self, frame: &mut Frame, pos: Pos, size: Size, layout: &Layout) { + for block in &layout.blocks { + match &block.content { + BlockContent::Msg(msg) => { + let time = format!("{}", msg.time.format("%h:%m")); + frame.write(pos, &time, ContentStyle::default()); + + let nick_width = frame.width(&msg.nick) as i32; + for (i, line) in msg.lines.iter().enumerate() { + let y = pos.y + block.line + i as i32; + if y < 0 || y >= size.height as i32 { + continue; + } + + self.render_indentation(frame, Pos::new(pos.x, y), block.indent); + let after_indentation = + pos.x + (TIME_WIDTH + 1 + INDENT_WIDTH * block.indent) as i32; + if i == 0 { + let nick_x = after_indentation; + let nick = format!("[{}]", msg.nick); + frame.write(Pos::new(nick_x, y), &nick, ContentStyle::default()); + } + let msg_x = after_indentation + 1 + nick_width + 2; + frame.write(Pos::new(msg_x, y), line, ContentStyle::default()); + } + } + BlockContent::Placeholder => { + self.render_indentation(frame, pos, block.indent); + let x = pos.x + (TIME_WIDTH + 1 + INDENT_WIDTH * block.indent) as i32; + let y = pos.y + block.line; + frame.write(Pos::new(x, y), "[...]", ContentStyle::default()); + } + } + } + } + pub fn handle_key_event>( &mut self, store: &mut S, room: &str, cursor: &mut Option>, event: KeyEvent, + frame: &mut Frame, size: Size, ) { // TODO } - pub fn render>( + pub async fn render>( &mut self, store: &mut S, room: &str, @@ -205,7 +382,7 @@ impl TreeView { pos: Pos, size: Size, ) { - // TODO - frame.write(Pos::new(0, 0), "Hello world!", ContentStyle::default()); + let layout = self.layout(room, store, cursor, frame, size).await; + self.render_layout(frame, pos, size, &layout); } } diff --git a/cove-tui/src/store.rs b/cove-tui/src/store.rs index 9906506..7a95917 100644 --- a/cove-tui/src/store.rs +++ b/cove-tui/src/store.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; pub trait Msg { - type Id: Hash + Eq; + type Id: Clone + Hash + Eq; fn id(&self) -> Self::Id; fn parent(&self) -> Option; diff --git a/cove-tui/src/ui.rs b/cove-tui/src/ui.rs index ce52bfa..2b1a7dc 100644 --- a/cove-tui/src/ui.rs +++ b/cove-tui/src/ui.rs @@ -107,7 +107,9 @@ impl Ui { let size = terminal.frame().size(); let result = match event { UiEvent::Redraw => EventHandleResult::Continue, - UiEvent::Term(Event::Key(event)) => self.handle_key_event(event, size).await, + UiEvent::Term(Event::Key(event)) => { + self.handle_key_event(event, terminal.frame(), size).await + } UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?, UiEvent::Term(Event::Resize(_, _)) => EventHandleResult::Continue, }; @@ -125,15 +127,20 @@ impl Ui { } async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> { - self.chat.render(frame, Pos::new(0, 0), frame.size()); + self.chat.render(frame, Pos::new(0, 0), frame.size()).await; Ok(()) } - async fn handle_key_event(&mut self, event: KeyEvent, size: Size) -> EventHandleResult { + async fn handle_key_event( + &mut self, + event: KeyEvent, + frame: &mut Frame, + size: Size, + ) -> EventHandleResult { if let KeyCode::Char('Q') = event.code { return EventHandleResult::Stop; } - self.chat.handle_key_event(event, size); + self.chat.handle_key_event(event, frame, size); EventHandleResult::Continue }