Adapt blocks to include markers

This commit is contained in:
Joscha 2022-07-15 11:06:48 +02:00
parent 26e988114c
commit 21d908874d
4 changed files with 112 additions and 99 deletions

15
src/macros.rs Normal file
View file

@ -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;

View file

@ -5,6 +5,7 @@
mod euph; mod euph;
mod export; mod export;
mod logger; mod logger;
mod macros;
mod replies; mod replies;
mod store; mod store;
mod ui; mod ui;

View file

@ -1,5 +1,5 @@
// mod action; // mod action;
// mod blocks; mod blocks;
// mod cursor; // mod cursor;
// mod layout; // mod layout;
// mod render; // mod render;

View file

@ -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 std::collections::VecDeque;
use chrono::{DateTime, Utc};
use toss::styled::Styled; use toss::styled::Styled;
use super::{util, Cursor}; use crate::macros::some_or_return;
pub struct Block<I> { pub enum MarkerBlock<I> {
pub id: I, After(I),
pub line: i32, Bottom,
pub height: i32,
pub cursor: bool,
pub time: Option<DateTime<Utc>>,
pub indent: usize,
pub body: BlockBody,
} }
impl<I> Block<I> { pub enum MsgContent {
pub fn msg( Msg { nick: Styled, lines: Vec<Styled> },
id: I,
indent: usize,
time: DateTime<Utc>,
nick: Styled,
lines: Vec<Styled>,
) -> 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<Utc>) -> Self {
self.time = Some(time);
self
}
}
pub enum BlockBody {
Msg(MsgBlock),
Placeholder, Placeholder,
} }
pub struct MsgBlock { pub struct MsgBlock<I> {
pub nick: Styled, id: I,
pub lines: Vec<Styled>, cursor: bool,
content: MsgContent,
}
impl<I> MsgBlock<I> {
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<I> {
Marker(MarkerBlock<I>),
Msg(MsgBlock<I>),
Compose(ComposeBlock),
}
pub struct Block<I> {
line: i32,
indent: usize,
body: BlockBody<I>,
}
impl<I> Block<I> {
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. /// Pre-layouted messages as a sequence of blocks.
/// ///
/// These blocks are straightforward to render, but also provide a level of /// These blocks are straightforward to render, but also provide a level of
/// abstraction between the layouting and actual displaying of messages. This /// abstraction between the layouting and actual displaying of messages.
/// might be useful in the future to ensure the cursor is always on a visible
/// message, for example.
/// ///
/// The following equation describes the relationship between the /// The following equation describes the relationship between the
/// [`Blocks::top_line`] and [`Blocks::bottom_line`] fields: /// [`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 /// 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 /// always the last line in a nonempty [`Blocks`]. In an empty layout, the
/// equation simplifies to /// equation simplifies to
/// ///
/// `top_line = bottom_line + 1` /// `bottom_line = top_line - 1`
pub struct Blocks<I> { pub struct Blocks<I> {
pub blocks: VecDeque<Block<I>>, pub blocks: VecDeque<Block<I>>,
/// The top line of the first block. Useful for prepending blocks, /// The top line of the first block. Useful for prepending blocks,
@ -90,66 +82,75 @@ pub struct Blocks<I> {
pub bottom_line: i32, pub bottom_line: i32,
} }
impl<I: PartialEq> Blocks<I> { impl<I> Blocks<I> {
pub fn new() -> Self { pub fn new(initial: Block<I>) -> Self {
Self::new_below(0) 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 pub fn new_empty() -> Self {
/// in `top_line = bottom_line = line`.
pub fn new_below(line: i32) -> Self {
Self { Self {
blocks: VecDeque::new(), blocks: VecDeque::new(),
top_line: line + 1, top_line: 0,
bottom_line: line, bottom_line: -1,
} }
} }
fn mark_cursor(&mut self, id: &I) -> usize { pub fn find<F>(&self, f: F) -> Option<&Block<I>>
let mut cursor = None; where
for (i, block) in self.blocks.iter_mut().enumerate() { F: Fn(&Block<I>) -> bool,
if &block.id == id { {
block.cursor = true; self.blocks.iter().find(|b| f(b))
if cursor.is_some() {
panic!("more than one cursor in blocks");
}
cursor = Some(i);
}
}
cursor.expect("no cursor in blocks")
} }
pub fn calculate_offsets_with_cursor(&mut self, cursor: &Cursor<I>, height: u16) { fn find_index_and_line<F>(&self, f: F) -> Option<(usize, i32)>
let cursor_index = self.mark_cursor(&cursor.id); where
let cursor_line = util::proportion_to_line(height, cursor.proportion); F: Fn(&Block<I>) -> Option<i32>,
{
self.blocks
.iter()
.enumerate()
.find_map(|(i, b)| f(b).map(|l| (i, l)))
}
// Propagate lines from cursor to both ends /// Update the offsets such that the line of the first block with a `Some`
self.blocks[cursor_index].line = cursor_line; /// return value becomes that value.
for i in (0..cursor_index).rev() { pub fn recalculate_offsets<F>(&mut self, f: F)
// let succ_line = self.0[i + 1].line; where
// let curr = &mut self.0[i]; F: Fn(&Block<I>) -> Option<i32>,
// curr.line = succ_line - curr.height; {
self.blocks[i].line = self.blocks[i + 1].line - self.blocks[i].height; 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() { for i in (idx + 1)..self.blocks.len() {
// let pred = &self.0[i - 1]; self.blocks[i].line = self.blocks[i - 1].line + self.blocks[i - 1].height();
// self.0[i].line = pred.line + pred.height;
self.blocks[i].line = self.blocks[i - 1].line + self.blocks[i - 1].height;
} }
self.top_line = self.blocks.front().expect("blocks nonempty").line; self.top_line = self.blocks.front().expect("blocks nonempty").line;
let bottom = self.blocks.back().expect("blocks nonempty"); 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<I>) { pub fn push_front(&mut self, mut block: Block<I>) {
self.top_line -= block.height; self.top_line -= block.height();
block.line = self.top_line; block.line = self.top_line;
self.blocks.push_front(block); self.blocks.push_front(block);
} }
pub fn push_back(&mut self, mut block: Block<I>) { pub fn push_back(&mut self, mut block: Block<I>) {
block.line = self.bottom_line + 1; block.line = self.bottom_line + 1;
self.bottom_line += block.height; self.bottom_line += block.height();
self.blocks.push_back(block); self.blocks.push_back(block);
} }
@ -172,8 +173,4 @@ impl<I: PartialEq> Blocks<I> {
block.line += delta; block.line += delta;
} }
} }
pub fn find(&self, id: &I) -> Option<&Block<I>> {
self.blocks.iter().find(|b| &b.id == id)
}
} }