diff --git a/src/ui/chat/tree/action.rs b/src/ui/chat/tree/action.rs deleted file mode 100644 index 32e4016..0000000 --- a/src/ui/chat/tree/action.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::sync::Arc; - -use parking_lot::FairMutex; -use toss::terminal::Terminal; - -use crate::store::{Msg, MsgStore}; -use crate::ui::util; - -use super::{Cursor, InnerTreeViewState}; - -impl> InnerTreeViewState { - pub async fn reply_normal( - &self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - ) -> Option<(Option, String)> { - match &self.cursor { - Cursor::Bottom => { - if let Some(content) = util::prompt(terminal, crossterm_lock) { - return Some((None, content)); - } - } - Cursor::Msg(msg) => { - let path = self.store.path(msg).await; - let tree = self.store.tree(path.first()).await; - let parent_id = if tree.next_sibling(msg).is_some() { - // A reply to a message that has further siblings should be a - // direct reply. An indirect reply might end up a lot further - // down in the current conversation. - msg.clone() - } else if let Some(parent) = tree.parent(msg) { - // A reply to a message without younger siblings should be - // an indirect reply so as not to create unnecessarily deep - // threads. In the case that our message has children, this - // might get a bit confusing. I'm not sure yet how well this - // "smart" reply actually works in practice. - parent - } else { - // When replying to a top-level message, it makes sense to avoid - // creating unnecessary new threads. - msg.clone() - }; - - if let Some(content) = util::prompt(terminal, crossterm_lock) { - return Some((Some(parent_id), content)); - } - } - _ => {} - } - - None - } - - /// Does approximately the opposite of [`Self::reply_normal`]. - pub async fn reply_alternate( - &self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - ) -> Option<(Option, String)> { - match &self.cursor { - Cursor::Bottom => { - if let Some(content) = util::prompt(terminal, crossterm_lock) { - return Some((None, content)); - } - } - Cursor::Msg(msg) => { - let path = self.store.path(msg).await; - let tree = self.store.tree(path.first()).await; - let parent_id = if tree.next_sibling(msg).is_none() { - // The opposite of replying normally - msg.clone() - } else if let Some(parent) = tree.parent(msg) { - // The opposite of replying normally - parent - } else { - // The same as replying normally, still to avoid creating - // unnecessary new threads - msg.clone() - }; - - if let Some(content) = util::prompt(terminal, crossterm_lock) { - return Some((Some(parent_id), content)); - } - } - _ => {} - } - - None - } - - pub fn create_new_thread( - terminal: &mut Terminal, - crossterm_lock: &Arc>, - ) -> Option<(Option, String)> { - util::prompt(terminal, crossterm_lock).map(|content| (None, content)) - } -} diff --git a/src/ui/chat/tree/blocks.rs b/src/ui/chat/tree/blocks.rs deleted file mode 100644 index 5d8786f..0000000 --- a/src/ui/chat/tree/blocks.rs +++ /dev/null @@ -1,267 +0,0 @@ -//! Intermediate representation of chat history as blocks of things. - -use std::collections::{vec_deque, VecDeque}; -use std::iter; - -use time::OffsetDateTime; -use toss::styled::Styled; - -use crate::macros::some_or_return; - -#[derive(Debug, Clone, Copy)] -pub enum MarkerBlock { - After(I), // TODO Is this marker necessary? - Bottom, -} - -#[derive(Debug, Clone)] -pub enum MsgContent { - Msg { nick: Styled, lines: Vec }, - Placeholder, -} - -#[derive(Debug, Clone)] -pub struct MsgBlock { - pub id: I, - pub content: MsgContent, -} - -impl MsgBlock { - pub fn height(&self) -> i32 { - match &self.content { - MsgContent::Msg { lines, .. } => lines.len() as i32, - MsgContent::Placeholder => 1, - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ComposeBlock { - // TODO Editor widget -} - -#[derive(Debug, Clone)] -pub enum BlockBody { - Marker(MarkerBlock), - Msg(MsgBlock), - Compose(ComposeBlock), -} - -#[derive(Debug, Clone)] -pub struct Block { - pub line: i32, - pub time: Option, - pub indent: usize, - pub body: BlockBody, -} - -impl Block { - pub fn bottom(line: i32) -> Self { - Self { - line, - time: None, - indent: 0, - body: BlockBody::Marker(MarkerBlock::Bottom), - } - } - - pub fn after(indent: usize, id: I) -> Self { - Self { - line: 0, - time: None, - indent, - body: BlockBody::Marker(MarkerBlock::After(id)), - } - } - - pub fn msg( - time: OffsetDateTime, - indent: usize, - id: I, - nick: Styled, - lines: Vec, - ) -> Self { - Self { - line: 0, - time: Some(time), - indent, - body: BlockBody::Msg(MsgBlock { - id, - content: MsgContent::Msg { nick, lines }, - }), - } - } - - pub fn placeholder(time: Option, indent: usize, id: I) -> Self { - Self { - line: 0, - time, - indent, - body: BlockBody::Msg(MsgBlock { - id, - content: MsgContent::Placeholder, - }), - } - } - - pub fn height(&self) -> i32 { - match &self.body { - BlockBody::Marker(m) => 0, - BlockBody::Msg(m) => m.height(), - BlockBody::Compose(e) => todo!(), - } - } -} - -/// Pre-layouted messages as a sequence of blocks. -/// -/// These blocks are straightforward to render, but also provide a level of -/// abstraction between the layouting and actual displaying of messages. -/// -/// The following equation describes the relationship between the -/// [`Blocks::top_line`] and [`Blocks::bottom_line`] fields: -/// -/// `bottom_line - top_line = sum of all heights - 1` -/// -/// This ensures that `top_line` is always the first line and `bottom_line` is -/// always the last line in a nonempty [`Blocks`]. In an empty layout, the -/// equation simplifies to -/// -/// `bottom_line = top_line - 1` -#[derive(Debug, Clone)] -pub struct Blocks { - pub blocks: VecDeque>, - /// The top line of the first block. Useful for prepending blocks, - /// especially to empty [`Blocks`]s. - pub top_line: i32, - /// The bottom line of the last block. Useful for appending blocks, - /// especially to empty [`Blocks`]s. - pub bottom_line: i32, - /// The root of the first and last tree, if any. Useful for figuring out - /// which blocks to prepend or append. - pub roots: Option<(I, I)>, -} - -impl Blocks { - pub fn new() -> Self { - Self { - blocks: VecDeque::new(), - top_line: 1, - bottom_line: 0, - roots: None, - } - } - - pub fn new_bottom(line: i32) -> Self { - Self { - blocks: iter::once(Block::bottom(line)).collect(), - top_line: line, - bottom_line: line - 1, - roots: None, - } - } - - pub fn find(&self, f: F) -> Option<&Block> - where - F: Fn(&Block) -> bool, - { - self.blocks.iter().find(|b| f(b)) - } - - pub fn iter(&self) -> vec_deque::Iter> { - self.blocks.iter() - } - - pub fn update(&mut self, f: F) - where - F: Fn(&mut Block), - { - for block in &mut self.blocks { - f(block); - } - } - - fn find_index_and_line(&self, f: F) -> Option<(usize, i32)> - where - F: Fn(&Block) -> Option, - { - self.blocks - .iter() - .enumerate() - .find_map(|(i, b)| f(b).map(|l| (i, l))) - } - - /// Update the offsets such that the line of the first block with a `Some` - /// return value becomes that value. - pub fn recalculate_offsets(&mut self, f: F) - where - F: Fn(&Block) -> Option, - { - let (idx, line) = some_or_return!(self.find_index_and_line(f)); - - // Propagate lines from index to both ends - self.blocks[idx].line = line; - for i in (0..idx).rev() { - self.blocks[i].line = self.blocks[i + 1].line - self.blocks[i].height(); - } - for i in (idx + 1)..self.blocks.len() { - self.blocks[i].line = self.blocks[i - 1].line + self.blocks[i - 1].height(); - } - - self.top_line = self.blocks.front().expect("blocks nonempty").line; - let bottom = self.blocks.back().expect("blocks nonempty"); - self.bottom_line = bottom.line + bottom.height() - 1; - } - - pub fn push_front(&mut self, mut block: Block) { - self.top_line -= block.height(); - block.line = self.top_line; - self.blocks.push_front(block); - } - - pub fn push_back(&mut self, mut block: Block) { - block.line = self.bottom_line + 1; - self.bottom_line += block.height(); - self.blocks.push_back(block); - } - - pub fn offset(&mut self, delta: i32) { - self.top_line += delta; - self.bottom_line += delta; - for block in &mut self.blocks { - block.line += delta; - } - } -} - -impl Blocks { - pub fn prepend(&mut self, mut layout: Self) { - while let Some(block) = layout.blocks.pop_back() { - self.push_front(block); - } - - if let Some((l_root_top, l_root_bot)) = layout.roots { - if let Some((root_top, _)) = &mut self.roots { - assert!(l_root_bot < *root_top); - *root_top = l_root_top; - } else { - self.roots = Some((l_root_top, l_root_bot)); - } - } - } - - pub fn append(&mut self, mut layout: Self) { - while let Some(block) = layout.blocks.pop_front() { - self.push_back(block); - } - - if let Some((l_root_top, l_root_bot)) = layout.roots { - if let Some((_, root_bot)) = &mut self.roots { - assert!(l_root_top > *root_bot); - *root_bot = l_root_bot; - } else { - self.roots = Some((l_root_top, l_root_bot)); - } - } - } -} diff --git a/src/ui/chat/tree/render.rs b/src/ui/chat/tree/render.rs deleted file mode 100644 index e649e55..0000000 --- a/src/ui/chat/tree/render.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Rendering blocks to a [`Frame`]. - -use time::OffsetDateTime; -use toss::frame::{Frame, Pos}; -use toss::styled::Styled; - -use crate::store::{Msg, MsgStore}; - -use super::blocks::{Block, BlockBody, MsgBlock, MsgContent}; -use super::{util, InnerTreeViewState}; - -impl> InnerTreeViewState { - fn render_time(frame: &mut Frame, line: i32, time: Option, is_cursor: bool) { - let pos = Pos::new(0, line); - let style = if is_cursor { - util::style_time_inverted() - } else { - util::style_time() - }; - - if let Some(time) = time { - let time = time - .format(util::TIME_FORMAT) - .expect("time can be formatted"); - frame.write(pos, (&time, style)); - } else { - frame.write(pos, (util::TIME_EMPTY, style)); - } - } - - fn render_indent(frame: &mut Frame, line: i32, indent: usize, is_cursor: bool) { - let pos = Pos::new(util::after_indent(0), line); - let style = if is_cursor { - util::style_indent_inverted() - } else { - util::style_indent() - }; - - let mut styled = Styled::default(); - for _ in 0..indent { - styled = styled.then((util::INDENT, style)); - } - - frame.write(pos, styled); - } - - fn render_nick(frame: &mut Frame, line: i32, indent: usize, nick: Styled) { - let nick_pos = Pos::new(util::after_indent(indent), line); - let styled = Styled::new("[").and_then(nick).then("]"); - frame.write(nick_pos, styled); - } - - fn draw_msg_block( - frame: &mut Frame, - line: i32, - time: Option, - indent: usize, - msg: &MsgBlock, - is_cursor: bool, - ) { - match &msg.content { - MsgContent::Msg { nick, lines } => { - let height: i32 = frame.size().height.into(); - let after_nick = util::after_nick(frame, indent, nick); - - for (i, text) in lines.iter().enumerate() { - let line = line + i as i32; - if line < 0 || line >= height { - continue; - } - - if i == 0 { - Self::render_indent(frame, line, indent, is_cursor); - Self::render_time(frame, line, time, is_cursor); - Self::render_nick(frame, line, indent, nick.clone()); - } else { - Self::render_indent(frame, line, indent + 1, false); - Self::render_indent(frame, line, indent, is_cursor); - Self::render_time(frame, line, None, is_cursor); - } - - frame.write(Pos::new(after_nick, line), text.clone()); - } - } - MsgContent::Placeholder => { - Self::render_time(frame, line, time, is_cursor); - Self::render_indent(frame, line, indent, is_cursor); - let pos = Pos::new(util::after_indent(indent), line); - frame.write(pos, (util::PLACEHOLDER, util::style_placeholder())); - } - } - } - - fn draw_block(frame: &mut Frame, block: &Block, is_cursor: bool) { - match &block.body { - BlockBody::Marker(_) => {} - BlockBody::Msg(msg) => { - Self::draw_msg_block(frame, block.line, block.time, block.indent, msg, is_cursor) - } - BlockBody::Compose(_) => {} - } - } - - pub fn draw_blocks(&self, frame: &mut Frame) { - for block in self.last_blocks.iter() { - Self::draw_block(frame, block, self.cursor.matches_block(block)); - } - } -} diff --git a/src/ui/chat/tree/util.rs b/src/ui/chat/tree/util.rs deleted file mode 100644 index 2683acb..0000000 --- a/src/ui/chat/tree/util.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Constants and helper functions. - -use crossterm::style::{ContentStyle, Stylize}; -use time::format_description::FormatItem; -use time::macros::format_description; -use toss::frame::Frame; -use toss::styled::Styled; - -pub const TIME_FORMAT: &[FormatItem<'_>] = - format_description!("[year]-[month]-[day] [hour]:[minute] "); -pub const TIME_EMPTY: &str = " "; -pub const TIME_WIDTH: usize = TIME_EMPTY.len(); - -pub fn style_time() -> ContentStyle { - ContentStyle::default().grey() -} - -pub fn style_time_inverted() -> ContentStyle { - ContentStyle::default().black().on_white() -} - -pub const INDENT: &str = "│ "; -pub const INDENT_WIDTH: usize = 2; - -pub fn style_indent() -> ContentStyle { - ContentStyle::default().dark_grey() -} - -pub fn style_indent_inverted() -> ContentStyle { - ContentStyle::default().black().on_white() -} - -pub const PLACEHOLDER: &str = "[...]"; - -pub fn style_placeholder() -> ContentStyle { - ContentStyle::default().dark_grey() -} - -pub const MIN_CONTENT_WIDTH: usize = "[+, 1234 more]".len(); - -pub fn after_indent(indent: usize) -> i32 { - (TIME_WIDTH + indent * INDENT_WIDTH) as i32 -} - -pub fn after_nick(frame: &mut Frame, indent: usize, nick: &Styled) -> i32 { - after_indent(indent) + 1 + frame.width_styled(nick) as i32 + 2 -}