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) { pub fn render(&mut self, frame: &mut Frame, pos: Pos, size: Size) {
match self.mode { match self.mode {
Mode::Tree => self Mode::Tree => {
.tree self.tree
.render(&mut self.store, &self.room, frame, pos, size), .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 crossterm::style::ContentStyle;
use toss::frame::{Frame, Pos, Size}; use toss::frame::{Frame, Pos, Size};
use crate::store::{Msg, MsgStore}; use crate::store::{Msg, MsgStore, Tree};
use super::Cursor; use super::Cursor;
@ -30,61 +30,97 @@ struct MsgBlock {
content: Vec<String>, 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> { impl<I: PartialEq> Layout<I> {
pub fn new() -> Self { fn new() -> Self {
Self(VecDeque::new()) Self::new_below(0)
} }
fn mark_cursor(&mut self, id: &I) { /// Create a new [`Layout`] such that prepending a single line will result
for block in &mut self.0 { /// in `top_line = bottom_line = line`.
if block.id.as_ref() == Some(id) { fn new_below(line: i32) -> Self {
block.cursor = true; Self {
} blocks: VecDeque::new(),
top_line: line + 1,
bottom_line: line,
} }
} }
fn calculate_offsets_with_cursor(&mut self, line: i32) { fn mark_cursor(&mut self, id: &I) -> usize {
let cursor_index = self let mut cursor = None;
.0 for (i, block) in self.blocks.iter_mut().enumerate() {
.iter() if block.id.as_ref() == Some(id) {
.enumerate() block.cursor = true;
.find(|(_, b)| b.cursor) if cursor.is_some() {
.expect("layout must contain cursor block") panic!("more than one cursor in layout");
.0; }
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 // 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() { for i in (0..cursor_index).rev() {
// let succ_line = self.0[i + 1].line; // let succ_line = self.0[i + 1].line;
// let curr = &mut self.0[i]; // let curr = &mut self.0[i];
// curr.line = succ_line - curr.height; // 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]; // let pred = &self.0[i - 1];
// self.0[i].line = pred.line + pred.height; // 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) { fn append(&mut self, mut layout: Self) {
if let Some(back) = self.0.back_mut() { while let Some(mut block) = layout.blocks.pop_front() {
back.line = height - back.height; block.line = self.bottom_line + 1;
} self.bottom_line += block.height;
for i in (0..self.0.len() - 1).rev() { self.blocks.push_back(block);
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);
} }
} }
} }
@ -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>>( async fn layout<S: MsgStore<M>>(
&mut self, &mut self,
room: &str, room: &str,
store: S, store: S,
cursor: &mut Option<Cursor<M::Id>>, cursor: &Option<Cursor<M::Id>>,
size: Size,
) -> Layout<M::Id> { ) -> Layout<M::Id> {
let height: i32 = size.height.into();
if let Some(cursor) = cursor { if let Some(cursor) = cursor {
// TODO Ensure focus lies on cursor path, otherwise unfocus // TODO Ensure focus lies on cursor path, otherwise unfocus
// TODO Unfold all messages on path to cursor // 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; 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 Expand layout upwards and downwards if there is no focus
todo!() todo!()
} else { } else {
// TODO Ensure there is no focus // TODO Ensure there is no focus
// TODO Produce layout of last tree (with correct offsets)
// TODO Expand layout upwards // Start layout at the bottom of the screen
todo!() 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, &mut self,
store: &mut S, store: &mut S,
room: &str, room: &str,
cursor: &Option<Cursor<M::Id>>,
frame: &mut Frame, frame: &mut Frame,
pos: Pos, pos: Pos,
size: Size, size: Size,

View file

@ -85,5 +85,9 @@ impl<M: Msg> Tree<M> {
#[async_trait] #[async_trait]
pub trait MsgStore<M: Msg> { pub trait MsgStore<M: Msg> {
async fn path(&self, room: &str, id: &M::Id) -> Path<M::Id>; 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::collections::{HashMap, HashSet};
use std::thread::Thread;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, TimeZone, Utc}; 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] #[async_trait]
@ -105,9 +122,35 @@ impl MsgStore<DummyMsg> for DummyStore {
Path::new(segments) 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![]; let mut msgs = vec![];
self.collect_tree(*root, &mut msgs); self.collect_tree(*root, &mut msgs);
Tree::new(*root, 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()
}
} }