diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..d1036dd --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,15 @@ +macro_rules! some_or_return { + ($e:expr) => { + match $e { + None => return, + Some(result) => result, + } + }; + ($e:expr, $ret:expr) => { + match $e { + None => return $ret, + Some(result) => result, + } + }; +} +pub(crate) use some_or_return; diff --git a/src/main.rs b/src/main.rs index 5efbed6..22b5113 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod euph; mod export; mod logger; +mod macros; mod replies; mod store; mod ui; diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index 7525935..9fb3424 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -1,5 +1,5 @@ // mod action; -// mod blocks; +mod blocks; // mod cursor; // mod layout; // mod render; diff --git a/src/ui/chat/tree/blocks.rs b/src/ui/chat/tree/blocks.rs index 8be0adb..6b971f5 100644 --- a/src/ui/chat/tree/blocks.rs +++ b/src/ui/chat/tree/blocks.rs @@ -1,85 +1,77 @@ -//! Intermediate representation of messages as blocks of lines. +//! Intermediate representation of chat history as blocks of things. use std::collections::VecDeque; -use chrono::{DateTime, Utc}; use toss::styled::Styled; -use super::{util, Cursor}; +use crate::macros::some_or_return; -pub struct Block { - pub id: I, - pub line: i32, - pub height: i32, - pub cursor: bool, - pub time: Option>, - pub indent: usize, - pub body: BlockBody, +pub enum MarkerBlock { + After(I), + Bottom, } -impl Block { - pub fn msg( - id: I, - indent: usize, - time: DateTime, - nick: Styled, - lines: Vec, - ) -> Self { - Self { - id, - line: 0, - height: lines.len() as i32, - indent, - time: Some(time), - cursor: false, - body: BlockBody::Msg(MsgBlock { nick, lines }), - } - } - - pub fn placeholder(id: I, indent: usize) -> Self { - Self { - id, - line: 0, - height: 1, - indent, - time: None, - cursor: false, - body: BlockBody::Placeholder, - } - } - - pub fn time(mut self, time: DateTime) -> Self { - self.time = Some(time); - self - } -} -pub enum BlockBody { - Msg(MsgBlock), +pub enum MsgContent { + Msg { nick: Styled, lines: Vec }, Placeholder, } -pub struct MsgBlock { - pub nick: Styled, - pub lines: Vec, +pub struct MsgBlock { + id: I, + cursor: bool, + content: MsgContent, +} + +impl MsgBlock { + pub fn height(&self) -> i32 { + match &self.content { + MsgContent::Msg { lines, .. } => lines.len() as i32, + MsgContent::Placeholder => 1, + } + } +} + +pub struct ComposeBlock { + // TODO Editor widget +} + +pub enum BlockBody { + Marker(MarkerBlock), + Msg(MsgBlock), + Compose(ComposeBlock), +} + +pub struct Block { + line: i32, + indent: usize, + body: BlockBody, +} + +impl Block { + 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. This -/// might be useful in the future to ensure the cursor is always on a visible -/// message, for example. +/// 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 + 1 = sum of all heights` +/// `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 /// -/// `top_line = bottom_line + 1` +/// `bottom_line = top_line - 1` pub struct Blocks { pub blocks: VecDeque>, /// The top line of the first block. Useful for prepending blocks, @@ -90,66 +82,75 @@ pub struct Blocks { pub bottom_line: i32, } -impl Blocks { - pub fn new() -> Self { - Self::new_below(0) +impl Blocks { + pub fn new(initial: Block) -> Self { + let top_line = initial.line; + let bottom_line = top_line + initial.height() - 1; + let mut blocks = VecDeque::new(); + blocks.push_back(initial); + Self { + blocks, + top_line, + bottom_line, + } } - /// Create a new [`Blocks`] such that prepending a single line will result - /// in `top_line = bottom_line = line`. - pub fn new_below(line: i32) -> Self { + pub fn new_empty() -> Self { Self { blocks: VecDeque::new(), - top_line: line + 1, - bottom_line: line, + top_line: 0, + bottom_line: -1, } } - fn mark_cursor(&mut self, id: &I) -> usize { - let mut cursor = None; - for (i, block) in self.blocks.iter_mut().enumerate() { - if &block.id == id { - block.cursor = true; - if cursor.is_some() { - panic!("more than one cursor in blocks"); - } - cursor = Some(i); - } - } - cursor.expect("no cursor in blocks") + pub fn find(&self, f: F) -> Option<&Block> + where + F: Fn(&Block) -> bool, + { + self.blocks.iter().find(|b| f(b)) } - pub fn calculate_offsets_with_cursor(&mut self, cursor: &Cursor, height: u16) { - let cursor_index = self.mark_cursor(&cursor.id); - let cursor_line = util::proportion_to_line(height, cursor.proportion); + 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))) + } - // Propagate lines from cursor to both ends - self.blocks[cursor_index].line = cursor_line; - for i in (0..cursor_index).rev() { - // let succ_line = self.0[i + 1].line; - // let curr = &mut self.0[i]; - // curr.line = succ_line - curr.height; - self.blocks[i].line = self.blocks[i + 1].line - self.blocks[i].height; + /// 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 (cursor_index + 1)..self.blocks.len() { - // let pred = &self.0[i - 1]; - // self.0[i].line = pred.line + pred.height; - self.blocks[i].line = self.blocks[i - 1].line + self.blocks[i - 1].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; + self.bottom_line = bottom.line + bottom.height() - 1; } pub fn push_front(&mut self, mut block: Block) { - self.top_line -= block.height; + 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.bottom_line += block.height(); self.blocks.push_back(block); } @@ -172,8 +173,4 @@ impl Blocks { block.line += delta; } } - - pub fn find(&self, id: &I) -> Option<&Block> { - self.blocks.iter().find(|b| &b.id == id) - } }