Start working on tree layouting

This commit is contained in:
Joscha 2022-06-13 20:00:23 +02:00
parent 6fdce9db1e
commit 021d5a8943
4 changed files with 159 additions and 49 deletions

View file

@ -57,9 +57,10 @@ impl<M: Msg, S: MsgStore<M>> Chat<M, S> {
pub fn render(&mut self, frame: &mut Frame, pos: Pos, size: Size) {
match self.mode {
Mode::Tree => self
.tree
.render(&mut self.store, &self.room, frame, pos, size),
Mode::Tree => {
self.tree
.render(&mut self.store, &self.room, &self.cursor, frame, pos, size)
}
}
}
}

View file

@ -6,7 +6,7 @@ use crossterm::event::KeyEvent;
use crossterm::style::ContentStyle;
use toss::frame::{Frame, Pos, Size};
use crate::store::{Msg, MsgStore};
use crate::store::{Msg, MsgStore, Tree};
use super::Cursor;
@ -30,61 +30,97 @@ struct MsgBlock {
content: Vec<String>,
}
struct Layout<I>(VecDeque<Block<I>>);
/// 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.
///
/// The following equation describes the relationship between the
/// [`Layout::top_line`] and [`Layout::bottom_line`] fields:
///
/// `bottom_line - top_line + 1 = sum of all heights`
///
/// This ensures that `top_line` is always the first line and `bottom_line` is
/// always the last line in a nonempty [`Layout`]. In an empty layout, the
/// equation simplifies to
///
/// `top_line = bottom_line + 1`
struct Layout<I> {
blocks: VecDeque<Block<I>>,
/// The top line of the first block. Useful for prepending blocks,
/// especially to empty [`Layout`]s.
top_line: i32,
/// The bottom line of the last block. Useful for appending blocks,
/// especially to empty [`Layout`]s.
bottom_line: i32,
}
impl<I: PartialEq> Layout<I> {
pub fn new() -> Self {
Self(VecDeque::new())
fn new() -> Self {
Self::new_below(0)
}
fn mark_cursor(&mut self, id: &I) {
for block in &mut self.0 {
if block.id.as_ref() == Some(id) {
block.cursor = true;
}
/// Create a new [`Layout`] such that prepending a single line will result
/// in `top_line = bottom_line = line`.
fn new_below(line: i32) -> Self {
Self {
blocks: VecDeque::new(),
top_line: line + 1,
bottom_line: line,
}
}
fn calculate_offsets_with_cursor(&mut self, line: i32) {
let cursor_index = self
.0
.iter()
.enumerate()
.find(|(_, b)| b.cursor)
.expect("layout must contain cursor block")
.0;
fn mark_cursor(&mut self, id: &I) -> usize {
let mut cursor = None;
for (i, block) in self.blocks.iter_mut().enumerate() {
if block.id.as_ref() == Some(id) {
block.cursor = true;
if cursor.is_some() {
panic!("more than one cursor in layout");
}
cursor = Some(i);
}
}
cursor.expect("no cursor in layout")
}
fn calculate_offsets_with_cursor(&mut self, cursor: &Cursor<I>, height: i32) {
let cursor_index = self.mark_cursor(&cursor.id);
let cursor_line = ((height - 1) as f32 * cursor.proportion).round() as i32;
// Propagate lines from cursor to both ends
self.0[cursor_index].line = line;
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.0[i].line = self.0[i + 1].line - self.0[i].height;
self.blocks[i].line = self.blocks[i + 1].line - self.blocks[i].height;
}
for i in (cursor_index + 1)..self.0.len() {
for i in (cursor_index + 1)..self.blocks.len() {
// let pred = &self.0[i - 1];
// self.0[i].line = pred.line + pred.height;
self.0[i].line = self.0[i - 1].line + self.0[i - 1].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;
let bottom = self.blocks.back().expect("blocks nonempty");
self.bottom_line = bottom.line + bottom.height - 1;
}
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);
}
}
fn calculate_offsets_without_cursor(&mut self, height: i32) {
if let Some(back) = self.0.back_mut() {
back.line = height - back.height;
}
for i in (0..self.0.len() - 1).rev() {
self.0[i].line = self.0[i + 1].line - self.0[i].height;
}
}
pub fn calculate_offsets(&mut self, height: i32, cursor: Option<Cursor<I>>) {
if let Some(cursor) = cursor {
let line = ((height - 1) as f32 * cursor.proportion) as i32;
self.mark_cursor(&cursor.id);
self.calculate_offsets_with_cursor(line);
} else {
self.calculate_offsets_without_cursor(height);
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);
}
}
}
@ -103,24 +139,49 @@ impl<M: Msg> TreeView<M> {
}
}
fn layout_tree(tree: Tree<M>) -> Layout<M::Id> {
todo!()
}
async fn layout<S: MsgStore<M>>(
&mut self,
room: &str,
store: S,
cursor: &mut Option<Cursor<M::Id>>,
cursor: &Option<Cursor<M::Id>>,
size: Size,
) -> Layout<M::Id> {
let height: i32 = size.height.into();
if let Some(cursor) = cursor {
// TODO Ensure focus lies on cursor path, otherwise unfocus
// TODO Unfold all messages on path to cursor
// Produce layout of cursor subtree (with correct offsets)
let cursor_path = store.path(room, &cursor.id).await;
// TODO Produce layout of cursor subtree (with correct offsets)
let cursor_tree = store.tree(room, cursor_path.first()).await;
let mut layout = Self::layout_tree(cursor_tree);
layout.calculate_offsets_with_cursor(cursor, height);
// TODO Expand layout upwards and downwards if there is no focus
todo!()
} else {
// TODO Ensure there is no focus
// TODO Produce layout of last tree (with correct offsets)
// TODO Expand layout upwards
todo!()
// Start layout at the bottom of the screen
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;
}
}
layout
}
}
@ -139,6 +200,7 @@ impl<M: Msg> TreeView<M> {
&mut self,
store: &mut S,
room: &str,
cursor: &Option<Cursor<M::Id>>,
frame: &mut Frame,
pos: Pos,
size: Size,

View file

@ -85,5 +85,9 @@ impl<M: Msg> Tree<M> {
#[async_trait]
pub trait MsgStore<M: Msg> {
async fn path(&self, room: &str, id: &M::Id) -> Path<M::Id>;
async fn thread(&self, room: &str, root: &M::Id) -> Tree<M>;
async fn tree(&self, room: &str, root: &M::Id) -> Tree<M>;
async fn prev_tree(&self, room: &str, tree: &M::Id) -> Option<M::Id>;
async fn next_tree(&self, room: &str, tree: &M::Id) -> Option<M::Id>;
async fn first_tree(&self, room: &str) -> Option<M::Id>;
async fn last_tree(&self, room: &str) -> Option<M::Id>;
}

View file

@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::thread::Thread;
use std::collections::{HashMap, HashSet};
use async_trait::async_trait;
use chrono::{DateTime, TimeZone, Utc};
@ -90,6 +89,24 @@ impl DummyStore {
}
}
}
fn trees(&self) -> Vec<usize> {
let mut trees = HashSet::new();
for m in self.msgs.values() {
match m.parent() {
Some(parent) if !self.msgs.contains_key(&parent) => {
trees.insert(parent);
}
Some(_) => {}
None => {
trees.insert(m.id());
}
}
}
let mut trees: Vec<usize> = trees.into_iter().collect();
trees.sort_unstable();
trees
}
}
#[async_trait]
@ -105,9 +122,35 @@ impl MsgStore<DummyMsg> for DummyStore {
Path::new(segments)
}
async fn thread(&self, _room: &str, root: &usize) -> Tree<DummyMsg> {
async fn tree(&self, _room: &str, root: &usize) -> Tree<DummyMsg> {
let mut msgs = vec![];
self.collect_tree(*root, &mut msgs);
Tree::new(*root, msgs)
}
async fn prev_tree(&self, _room: &str, tree: &usize) -> Option<usize> {
let trees = self.trees();
trees
.iter()
.zip(trees.iter().skip(1))
.find(|(_, t)| *t == tree)
.map(|(t, _)| *t)
}
async fn next_tree(&self, _room: &str, tree: &usize) -> Option<usize> {
let trees = self.trees();
trees
.iter()
.zip(trees.iter().skip(1))
.find(|(t, _)| *t == tree)
.map(|(_, t)| *t)
}
async fn first_tree(&self, _room: &str) -> Option<usize> {
self.trees().first().cloned()
}
async fn last_tree(&self, _room: &str) -> Option<usize> {
self.trees().last().cloned()
}
}