From 021d5a8943c632ca08154cdf53c1e616a151117e Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 13 Jun 2022 20:00:23 +0200 Subject: [PATCH] Start working on tree layouting --- cove-tui/src/chat.rs | 7 +- cove-tui/src/chat/tree.rs | 146 +++++++++++++++++++++++++----------- cove-tui/src/store.rs | 6 +- cove-tui/src/store/dummy.rs | 49 +++++++++++- 4 files changed, 159 insertions(+), 49 deletions(-) diff --git a/cove-tui/src/chat.rs b/cove-tui/src/chat.rs index 4c6829e..8067806 100644 --- a/cove-tui/src/chat.rs +++ b/cove-tui/src/chat.rs @@ -57,9 +57,10 @@ impl> Chat { 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) + } } } } diff --git a/cove-tui/src/chat/tree.rs b/cove-tui/src/chat/tree.rs index 6dba075..029f09a 100644 --- a/cove-tui/src/chat/tree.rs +++ b/cove-tui/src/chat/tree.rs @@ -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, } -struct Layout(VecDeque>); +/// 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 { + blocks: VecDeque>, + /// 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 Layout { - 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, 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>) { - 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 TreeView { } } + fn layout_tree(tree: Tree) -> Layout { + todo!() + } + async fn layout>( &mut self, room: &str, store: S, - cursor: &mut Option>, + cursor: &Option>, + size: Size, ) -> Layout { + 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 TreeView { &mut self, store: &mut S, room: &str, + cursor: &Option>, frame: &mut Frame, pos: Pos, size: Size, diff --git a/cove-tui/src/store.rs b/cove-tui/src/store.rs index 1ff56e1..9906506 100644 --- a/cove-tui/src/store.rs +++ b/cove-tui/src/store.rs @@ -85,5 +85,9 @@ impl Tree { #[async_trait] pub trait MsgStore { async fn path(&self, room: &str, id: &M::Id) -> Path; - async fn thread(&self, room: &str, root: &M::Id) -> Tree; + async fn tree(&self, room: &str, root: &M::Id) -> Tree; + async fn prev_tree(&self, room: &str, tree: &M::Id) -> Option; + async fn next_tree(&self, room: &str, tree: &M::Id) -> Option; + async fn first_tree(&self, room: &str) -> Option; + async fn last_tree(&self, room: &str) -> Option; } diff --git a/cove-tui/src/store/dummy.rs b/cove-tui/src/store/dummy.rs index 6b5d6b3..6f52559 100644 --- a/cove-tui/src/store/dummy.rs +++ b/cove-tui/src/store/dummy.rs @@ -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 { + 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 = trees.into_iter().collect(); + trees.sort_unstable(); + trees + } } #[async_trait] @@ -105,9 +122,35 @@ impl MsgStore for DummyStore { Path::new(segments) } - async fn thread(&self, _room: &str, root: &usize) -> Tree { + async fn tree(&self, _room: &str, root: &usize) -> Tree { let mut msgs = vec![]; self.collect_tree(*root, &mut msgs); Tree::new(*root, msgs) } + + async fn prev_tree(&self, _room: &str, tree: &usize) -> Option { + 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 { + 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 { + self.trees().first().cloned() + } + + async fn last_tree(&self, _room: &str) -> Option { + self.trees().last().cloned() + } }