diff --git a/src/store.rs b/src/store.rs index a5e312f..ce2a816 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; +use std::vec; use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -31,6 +32,10 @@ impl Path { &self.0 } + pub fn push(&mut self, segment: I) { + self.0.push(segment) + } + pub fn first(&self) -> &I { self.0.first().expect("path is not empty") } @@ -48,6 +53,15 @@ impl Path { } } +impl IntoIterator for Path { + type Item = I; + type IntoIter = vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + pub struct Tree { root: M::Id, msgs: HashMap, diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index 5b193bb..7aedf22 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -1,9 +1,9 @@ // mod action; mod blocks; // mod cursor; -// mod layout; +mod layout; // mod render; -// mod util; +mod util; use std::sync::Arc; @@ -14,6 +14,8 @@ use toss::frame::{Frame, Size}; use crate::store::{Msg, MsgStore}; use crate::ui::widgets::Widget; +use self::blocks::Blocks; + /////////// // State // /////////// @@ -45,7 +47,7 @@ enum Cursor { struct InnerTreeViewState> { store: S, - last_blocks: (), // TODO + last_blocks: Blocks, last_cursor: Cursor, cursor: Cursor, editor: (), // TODO @@ -55,7 +57,7 @@ impl> InnerTreeViewState { fn new(store: S) -> Self { Self { store, - last_blocks: (), + last_blocks: Blocks::new(), last_cursor: Cursor::Bottom, cursor: Cursor::Bottom, editor: (), diff --git a/src/ui/chat/tree/blocks.rs b/src/ui/chat/tree/blocks.rs index 2ded080..0c6f1aa 100644 --- a/src/ui/chat/tree/blocks.rs +++ b/src/ui/chat/tree/blocks.rs @@ -2,6 +2,7 @@ use std::collections::VecDeque; +use chrono::{DateTime, Utc}; use toss::styled::Styled; use crate::macros::some_or_return; @@ -42,6 +43,7 @@ pub enum BlockBody { pub struct Block { pub line: i32, + pub time: Option>, pub indent: usize, pub body: BlockBody, } @@ -50,6 +52,7 @@ impl Block { pub fn bottom() -> Self { Self { line: 0, + time: None, indent: 0, body: BlockBody::Marker(MarkerBlock::Bottom), } @@ -58,14 +61,22 @@ impl Block { pub fn after(indent: usize, id: I) -> Self { Self { line: 0, + time: None, indent, body: BlockBody::Marker(MarkerBlock::After(id)), } } - pub fn msg(indent: usize, id: I, nick: Styled, lines: Vec) -> Self { + pub fn msg( + time: DateTime, + indent: usize, + id: I, + nick: Styled, + lines: Vec, + ) -> Self { Self { line: 0, + time: Some(time), indent, body: BlockBody::Msg(MsgBlock { id, @@ -74,9 +85,10 @@ impl Block { } } - pub fn placeholder(indent: usize, id: I) -> Self { + pub fn placeholder(time: Option>, indent: usize, id: I) -> Self { Self { line: 0, + time, indent, body: BlockBody::Msg(MsgBlock { id, diff --git a/src/ui/chat/tree/layout.rs b/src/ui/chat/tree/layout.rs index e8f951b..37f2a90 100644 --- a/src/ui/chat/tree/layout.rs +++ b/src/ui/chat/tree/layout.rs @@ -2,102 +2,13 @@ use toss::frame::{Frame, Size}; -use crate::store::{Msg, MsgStore, Tree}; +use crate::store::{Msg, MsgStore, Path, Tree}; -use super::blocks::{Block, Blocks}; -use super::util::{self, MIN_CONTENT_WIDTH}; -use super::{Cursor, TreeView}; - -fn msg_to_block(frame: &mut Frame, size: Size, msg: &M, indent: usize) -> Block { - let nick = msg.nick(); - let content = msg.content(); - - let content_width = size.width as i32 - util::after_nick(frame, indent, &nick.text()); - if content_width < MIN_CONTENT_WIDTH as i32 { - Block::placeholder(msg.id(), indent).time(msg.time()) - } else { - let content_width = content_width as usize; - let breaks = frame.wrap(&content.text(), content_width); - let lines = content.split_at_indices(&breaks); - Block::msg(msg.id(), indent, msg.time(), nick, lines) - } -} - -fn layout_subtree( - frame: &mut Frame, - size: Size, - tree: &Tree, - indent: usize, - id: &M::Id, - result: &mut Blocks, -) { - let block = if let Some(msg) = tree.msg(id) { - msg_to_block(frame, size, msg, indent) - } else { - Block::placeholder(id.clone(), indent) - }; - result.push_back(block); - - if let Some(children) = tree.children(id) { - for child in children { - layout_subtree(frame, size, tree, indent + 1, child, result); - } - } -} - -fn layout_tree(frame: &mut Frame, size: Size, tree: Tree) -> Blocks { - let mut blocks = Blocks::new(); - layout_subtree(frame, size, &tree, 0, tree.root(), &mut blocks); - blocks -} +use super::blocks::{Block, BlockBody, Blocks, MarkerBlock}; +use super::{util, Cursor, InnerTreeViewState}; +/* impl TreeView { - pub async fn expand_blocks_up>( - store: &S, - frame: &mut Frame, - size: Size, - blocks: &mut Blocks, - tree_id: &mut Option, - ) { - while blocks.top_line > 0 { - *tree_id = if let Some(tree_id) = tree_id { - store.prev_tree(tree_id).await - } else { - break; - }; - - if let Some(tree_id) = tree_id { - let tree = store.tree(tree_id).await; - blocks.prepend(layout_tree(frame, size, tree)); - } else { - break; - } - } - } - - pub async fn expand_blocks_down>( - store: &S, - frame: &mut Frame, - size: Size, - blocks: &mut Blocks, - tree_id: &mut Option, - ) { - while blocks.bottom_line < size.height as i32 { - *tree_id = if let Some(tree_id) = tree_id { - store.next_tree(tree_id).await - } else { - break; - }; - - if let Some(tree_id) = tree_id { - let tree = store.tree(tree_id).await; - blocks.append(layout_tree(frame, size, tree)); - } else { - break; - } - } - } - // TODO Split up based on cursor presence pub async fn layout_blocks>( &mut self, @@ -165,3 +76,187 @@ impl TreeView { } } } +*/ + +impl Cursor { + fn matches_block(&self, block: &Block) -> bool { + match self { + Self::Bottom => matches!(&block.body, BlockBody::Marker(MarkerBlock::Bottom)), + Self::Msg(id) => matches!(&block.body, BlockBody::Msg(msg) if msg.id == *id), + Self::Compose(lc) | Self::Placeholder(lc) => match &lc.after { + Some(bid) => { + matches!(&block.body, BlockBody::Marker(MarkerBlock::After(aid)) if aid == bid) + } + None => matches!(&block.body, BlockBody::Marker(MarkerBlock::Bottom)), + }, + } + } +} + +impl> InnerTreeViewState { + async fn cursor_path(&self, cursor: &Cursor) -> Path { + match cursor { + Cursor::Bottom => match self.store.last_tree().await { + Some(id) => Path::new(vec![id]), + None => Path::new(vec![M::last_possible_id()]), + }, + Cursor::Msg(id) => self.store.path(id).await, + Cursor::Compose(lc) | Cursor::Placeholder(lc) => match &lc.after { + None => Path::new(vec![M::last_possible_id()]), + Some(id) => { + let mut path = self.store.path(id).await; + path.push(M::last_possible_id()); + path + } + }, + } + } + + fn cursor_tree_id<'a>( + cursor: &Cursor, + cursor_path: &'a Path, + ) -> Option<&'a M::Id> { + match cursor { + Cursor::Bottom => None, + Cursor::Msg(id) => Some(cursor_path.first()), + Cursor::Compose(lc) | Cursor::Placeholder(lc) => match &lc.after { + None => None, + Some(id) => Some(cursor_path.first()), + }, + } + } + + fn cursor_line( + last_blocks: &Blocks, + cursor: &Cursor, + cursor_path: &Path, + last_cursor_path: &Path, + size: Size, + ) -> i32 { + if let Some(block) = last_blocks.find(|b| cursor.matches_block(b)) { + block.line + } else if last_cursor_path < cursor_path { + // Not using size.height - 1 because markers like + // MarkerBlock::Bottom in the line below the last visible line are + // still relevant to us. + size.height.into() + } else { + 0 + } + } + + fn msg_to_block(frame: &mut Frame, indent: usize, msg: &M) -> Block { + let size = frame.size(); + + let nick = msg.nick(); + let content = msg.content(); + + let content_width = size.width as i32 - util::after_nick(frame, indent, &nick.text()); + if content_width < util::MIN_CONTENT_WIDTH as i32 { + Block::placeholder(Some(msg.time()), indent, msg.id()) + } else { + let content_width = content_width as usize; + let breaks = frame.wrap(&content.text(), content_width); + let lines = content.split_at_indices(&breaks); + Block::msg(msg.time(), indent, msg.id(), nick, lines) + } + } + + fn layout_subtree( + frame: &mut Frame, + tree: &Tree, + indent: usize, + id: &M::Id, + result: &mut Blocks, + ) { + let block = if let Some(msg) = tree.msg(id) { + Self::msg_to_block(frame, indent, msg) + } else { + Block::placeholder(None, indent, id.clone()) + }; + result.push_back(block); + + if let Some(children) = tree.children(id) { + for child in children { + Self::layout_subtree(frame, tree, indent + 1, child, result); + } + } + + result.push_back(Block::after(indent, id.clone())) + } + + fn layout_tree(frame: &mut Frame, tree: Tree) -> Blocks { + let mut blocks = Blocks::new(); + Self::layout_subtree(frame, &tree, 0, tree.root(), &mut blocks); + blocks + } + + /// Create a [`Blocks`] of the current cursor's immediate surroundings. + pub async fn layout_cursor_surroundings(&self, frame: &mut Frame) -> Blocks { + let size = frame.size(); + + let cursor_path = self.cursor_path(&self.cursor).await; + let last_cursor_path = self.cursor_path(&self.last_cursor).await; + let tree_id = Self::cursor_tree_id(&self.cursor, &cursor_path); + let cursor_line = Self::cursor_line( + &self.last_blocks, + &self.cursor, + &cursor_path, + &last_cursor_path, + size, + ); + + if let Some(tree_id) = tree_id { + let tree = self.store.tree(tree_id).await; + let mut blocks = Self::layout_tree(frame, tree); + blocks.recalculate_offsets(|b| { + if self.cursor.matches_block(b) { + Some(cursor_line) + } else { + None + } + }); + blocks + } else { + let mut blocks = Blocks::new_below(cursor_line); + blocks.push_front(Block::bottom()); + blocks + } + } + + pub async fn expand_blocks_up(&self, frame: &mut Frame, blocks: &mut Blocks) { + while blocks.top_line > 0 { + let tree_id = if let Some((root_top, _)) = &blocks.roots { + self.store.prev_tree(root_top).await + } else { + self.store.last_tree().await + }; + + if let Some(tree_id) = tree_id { + let tree = self.store.tree(&tree_id).await; + blocks.prepend(Self::layout_tree(frame, tree)); + } else { + break; + } + } + } + + pub async fn expand_blocks_down(&self, frame: &mut Frame, blocks: &mut Blocks) { + while blocks.bottom_line < frame.size().height as i32 { + let tree_id = if let Some((_, root_bot)) = &blocks.roots { + self.store.next_tree(root_bot).await + } else { + // We assume that a Blocks without roots is at the bottom of the + // room's history. Therefore, there are no more messages below. + break; + }; + + if let Some(tree_id) = tree_id { + let tree = self.store.tree(&tree_id).await; + blocks.append(Self::layout_tree(frame, tree)); + } else { + break; + } + } + } +}