Layout using new algorithm and new blocks
This commit is contained in:
parent
6f4d94afa5
commit
ae8ec70e5e
7 changed files with 459 additions and 254 deletions
|
|
@ -7,24 +7,24 @@ use crate::macros::some_or_return;
|
||||||
use crate::ui::widgets::BoxedWidget;
|
use crate::ui::widgets::BoxedWidget;
|
||||||
|
|
||||||
pub struct Block<I> {
|
pub struct Block<I> {
|
||||||
id: I,
|
pub id: I,
|
||||||
top_line: i32,
|
pub top_line: i32,
|
||||||
height: i32,
|
pub height: i32,
|
||||||
/// The lines of the block that should be made visible if the block is
|
/// The lines of the block that should be made visible if the block is
|
||||||
/// focused on. By default, the focus encompasses the entire block.
|
/// focused on. By default, the focus encompasses the entire block.
|
||||||
///
|
///
|
||||||
/// If not all of these lines can be made visible, the top of the range
|
/// If not all of these lines can be made visible, the top of the range
|
||||||
/// should be preferred over the bottom.
|
/// should be preferred over the bottom.
|
||||||
focus: Range<i32>,
|
pub focus: Range<i32>,
|
||||||
widget: BoxedWidget,
|
pub widget: BoxedWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> Block<I> {
|
impl<I> Block<I> {
|
||||||
pub fn new<W: Into<BoxedWidget>>(frame: &mut Frame, width: u16, id: I, widget: W) -> Self {
|
pub fn new<W: Into<BoxedWidget>>(frame: &mut Frame, id: I, widget: W) -> Self {
|
||||||
// Interestingly, rust-analyzer fails to deduce the type of `widget`
|
// Interestingly, rust-analyzer fails to deduce the type of `widget`
|
||||||
// here but rustc knows it's a `BoxedWidget`.
|
// here but rustc knows it's a `BoxedWidget`.
|
||||||
let widget = widget.into();
|
let widget = widget.into();
|
||||||
let size = widget.size(frame, Some(width), None);
|
let size = widget.size(frame, Some(frame.size().width), None);
|
||||||
let height = size.height.into();
|
let height = size.height.into();
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
|
@ -97,15 +97,55 @@ impl<I> Blocks<I> {
|
||||||
self.push_back(block);
|
self.push_back(block);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_top_line(&mut self, line: i32) {
|
||||||
|
self.top_line = line;
|
||||||
|
|
||||||
|
if let Some(first_block) = self.blocks.front_mut() {
|
||||||
|
first_block.top_line = self.top_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 1..self.blocks.len() {
|
||||||
|
self.blocks[i].top_line = self.blocks[i - 1].top_line + self.blocks[i - 1].height;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bottom_line = self
|
||||||
|
.blocks
|
||||||
|
.back()
|
||||||
|
.map(|b| b.top_line + b.height - 1)
|
||||||
|
.unwrap_or(self.top_line - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_bottom_line(&mut self, line: i32) {
|
||||||
|
self.bottom_line = line;
|
||||||
|
|
||||||
|
if let Some(last_block) = self.blocks.back_mut() {
|
||||||
|
last_block.top_line = self.bottom_line + 1 - last_block.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in (1..self.blocks.len()).rev() {
|
||||||
|
self.blocks[i - 1].top_line = self.blocks[i].top_line - self.blocks[i - 1].height;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.top_line = self
|
||||||
|
.blocks
|
||||||
|
.front()
|
||||||
|
.map(|b| b.top_line)
|
||||||
|
.unwrap_or(self.bottom_line + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I: Eq> Blocks<I> {
|
impl<I: Eq> Blocks<I> {
|
||||||
pub fn recalculate_offsets(&mut self, id: I, top_line: i32) {
|
pub fn find(&self, id: &I) -> Option<&Block<I>> {
|
||||||
|
self.blocks.iter().find(|b| b.id == *id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recalculate_offsets(&mut self, id: &I, top_line: i32) {
|
||||||
let idx = some_or_return!(self
|
let idx = some_or_return!(self
|
||||||
.blocks
|
.blocks
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, b)| b.id == id)
|
.find(|(_, b)| b.id == *id)
|
||||||
.map(|(i, _)| i));
|
.map(|(i, _)| i));
|
||||||
|
|
||||||
self.blocks[idx].top_line = top_line;
|
self.blocks[idx].top_line = top_line;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
mod layout;
|
||||||
mod time;
|
mod time;
|
||||||
|
mod tree_blocks;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -6,17 +9,20 @@ use async_trait::async_trait;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use toss::frame::{Frame, Size};
|
use toss::frame::{Frame, Pos, Size};
|
||||||
use toss::terminal::Terminal;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::store::{Msg, MsgStore};
|
use crate::store::{Msg, MsgStore};
|
||||||
use crate::ui::widgets::editor::EditorState;
|
use crate::ui::widgets::editor::EditorState;
|
||||||
use crate::ui::widgets::Widget;
|
use crate::ui::widgets::Widget;
|
||||||
|
|
||||||
|
use self::tree_blocks::TreeBlocks;
|
||||||
|
|
||||||
///////////
|
///////////
|
||||||
// State //
|
// State //
|
||||||
///////////
|
///////////
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Cursor<I> {
|
pub enum Cursor<I> {
|
||||||
Bottom,
|
Bottom,
|
||||||
Msg(I),
|
Msg(I),
|
||||||
|
|
@ -24,6 +30,24 @@ pub enum Cursor<I> {
|
||||||
Pseudo(Option<I>),
|
Pseudo(Option<I>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<I: Eq> Cursor<I> {
|
||||||
|
pub fn refers_to(&self, id: &I) -> bool {
|
||||||
|
if let Self::Msg(own_id) = self {
|
||||||
|
own_id == id
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refers_to_last_child_of(&self, id: &I) -> bool {
|
||||||
|
if let Self::Editor(Some(parent)) | Self::Pseudo(Some(parent)) = self {
|
||||||
|
parent == id
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct InnerTreeViewState<M: Msg, S: MsgStore<M>> {
|
struct InnerTreeViewState<M: Msg, S: MsgStore<M>> {
|
||||||
store: S,
|
store: S,
|
||||||
|
|
||||||
|
|
@ -113,5 +137,16 @@ where
|
||||||
|
|
||||||
async fn render(self: Box<Self>, frame: &mut Frame) {
|
async fn render(self: Box<Self>, frame: &mut Frame) {
|
||||||
let mut guard = self.0.lock().await;
|
let mut guard = self.0.lock().await;
|
||||||
|
let blocks = guard.relayout(frame).await;
|
||||||
|
|
||||||
|
let size = frame.size();
|
||||||
|
for block in blocks.into_blocks().blocks {
|
||||||
|
frame.push(
|
||||||
|
Pos::new(0, block.top_line),
|
||||||
|
Size::new(size.width, block.height as u16),
|
||||||
|
);
|
||||||
|
block.widget.render(frame).await;
|
||||||
|
frame.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,292 +1,310 @@
|
||||||
//! Arranging messages as blocks.
|
use toss::frame::Frame;
|
||||||
|
|
||||||
use toss::frame::{Frame, Size};
|
|
||||||
|
|
||||||
use crate::store::{Msg, MsgStore, Path, Tree};
|
use crate::store::{Msg, MsgStore, Path, Tree};
|
||||||
|
use crate::ui::chat::blocks::Block;
|
||||||
|
use crate::ui::widgets::empty::Empty;
|
||||||
|
use crate::ui::widgets::text::Text;
|
||||||
|
|
||||||
use super::blocks::{Block, BlockBody, Blocks, MsgBlock};
|
use super::tree_blocks::{BlockId, Root, TreeBlocks};
|
||||||
use super::cursor::Cursor;
|
use super::{widgets, Cursor, InnerTreeViewState};
|
||||||
use super::{util, InnerTreeViewState};
|
|
||||||
|
|
||||||
impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
||||||
async fn cursor_path(&self, cursor: &Cursor<M::Id>) -> Path<M::Id> {
|
async fn cursor_path(&self, cursor: &Cursor<M::Id>) -> Path<M::Id> {
|
||||||
match cursor {
|
match cursor {
|
||||||
Cursor::Bottom => match self.store.last_tree_id().await {
|
|
||||||
Some(id) => Path::new(vec![id]),
|
|
||||||
None => Path::new(vec![M::last_possible_id()]),
|
|
||||||
},
|
|
||||||
Cursor::Msg(id) => self.store.path(id).await,
|
Cursor::Msg(id) => self.store.path(id).await,
|
||||||
Cursor::Compose(lc) | Cursor::Placeholder(lc) => match &lc.after {
|
Cursor::Bottom | Cursor::Editor(None) | Cursor::Pseudo(None) => {
|
||||||
None => Path::new(vec![M::last_possible_id()]),
|
Path::new(vec![M::last_possible_id()])
|
||||||
Some(id) => {
|
}
|
||||||
let mut path = self.store.path(id).await;
|
Cursor::Editor(Some(parent)) | Cursor::Pseudo(Some(parent)) => {
|
||||||
path.push(M::last_possible_id());
|
let mut path = self.store.path(parent).await;
|
||||||
path
|
path.push(M::last_possible_id());
|
||||||
}
|
path
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursor_tree_id<'a>(
|
fn cursor_line(&self, blocks: &TreeBlocks<M::Id>) -> i32 {
|
||||||
cursor: &Cursor<M::Id>,
|
if let Cursor::Bottom = self.cursor {
|
||||||
cursor_path: &'a Path<M::Id>,
|
// The value doesn't matter as it will always be ignored.
|
||||||
) -> Option<&'a M::Id> {
|
|
||||||
match cursor {
|
|
||||||
Cursor::Bottom => None,
|
|
||||||
Cursor::Msg(_) => Some(cursor_path.first()),
|
|
||||||
Cursor::Compose(lc) | Cursor::Placeholder(lc) => match &lc.after {
|
|
||||||
None => None,
|
|
||||||
Some(_) => Some(cursor_path.first()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cursor_line(
|
|
||||||
last_blocks: &Blocks<M::Id>,
|
|
||||||
cursor: &Cursor<M::Id>,
|
|
||||||
cursor_path: &Path<M::Id>,
|
|
||||||
last_cursor_path: &Path<M::Id>,
|
|
||||||
size: Size,
|
|
||||||
) -> i32 {
|
|
||||||
if matches!(cursor, Cursor::Bottom) {
|
|
||||||
// Ensures that a Cursor::Bottom is always at the bottom of the
|
|
||||||
// screen. Will be scroll-clamped to the bottom later.
|
|
||||||
0
|
0
|
||||||
} else if let Some(block) = last_blocks.find(|b| cursor.matches_block(b)) {
|
|
||||||
block.line
|
|
||||||
} else if last_cursor_path < cursor_path {
|
|
||||||
// If the cursor is bottom, the bottom marker needs to be located at
|
|
||||||
// the line below the last visible line. If it is a normal message
|
|
||||||
// cursor, it will be made visible again one way or another later.
|
|
||||||
size.height.into()
|
|
||||||
} else {
|
} else {
|
||||||
0
|
blocks
|
||||||
|
.blocks()
|
||||||
|
.find(&BlockId::from_cursor(&self.cursor))
|
||||||
|
.expect("cursor is visible")
|
||||||
|
.top_line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn msg_to_block(frame: &mut Frame, indent: usize, msg: &M) -> Block<M::Id> {
|
fn contains_cursor(&self, blocks: &TreeBlocks<M::Id>) -> bool {
|
||||||
let size = frame.size();
|
blocks
|
||||||
|
.blocks()
|
||||||
let nick = msg.nick();
|
.find(&BlockId::from_cursor(&self.cursor))
|
||||||
let content = msg.content();
|
.is_some()
|
||||||
|
|
||||||
let content_width = size.width as i32 - util::after_nick(frame, indent, &nick);
|
|
||||||
if content_width < util::MIN_CONTENT_WIDTH as i32 {
|
|
||||||
Block::placeholder(Some(msg.time()), indent, msg.id())
|
|
||||||
} else {
|
|
||||||
let content_width = content_width as usize;
|
|
||||||
let breaks = frame.wrap(&content.text(), content_width);
|
|
||||||
let lines = content.split_at_indices(&breaks);
|
|
||||||
Block::msg(msg.time(), indent, msg.id(), nick, lines)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_subtree(
|
fn layout_subtree(
|
||||||
|
&self,
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
tree: &Tree<M>,
|
tree: &Tree<M>,
|
||||||
indent: usize,
|
indent: usize,
|
||||||
id: &M::Id,
|
id: &M::Id,
|
||||||
result: &mut Blocks<M::Id>,
|
blocks: &mut TreeBlocks<M::Id>,
|
||||||
) {
|
) {
|
||||||
let block = if let Some(msg) = tree.msg(id) {
|
// Ghost cursor in front, for positioning according to last cursor line
|
||||||
Self::msg_to_block(frame, indent, msg)
|
if self.last_cursor.refers_to(id) {
|
||||||
} else {
|
let block = Block::new(frame, BlockId::LastCursor, Empty);
|
||||||
Block::placeholder(None, indent, id.clone())
|
blocks.blocks_mut().push_back(block);
|
||||||
};
|
}
|
||||||
result.push_back(block);
|
|
||||||
|
|
||||||
|
// Main message body
|
||||||
|
let highlighted = self.cursor.refers_to(id);
|
||||||
|
let widget = if let Some(msg) = tree.msg(id) {
|
||||||
|
widgets::msg(highlighted, indent, msg)
|
||||||
|
} else {
|
||||||
|
widgets::msg_placeholder(highlighted, indent)
|
||||||
|
};
|
||||||
|
let block = Block::new(frame, BlockId::Msg(id.clone()), widget);
|
||||||
|
blocks.blocks_mut().push_back(block);
|
||||||
|
|
||||||
|
// Children, recursively
|
||||||
if let Some(children) = tree.children(id) {
|
if let Some(children) = tree.children(id) {
|
||||||
for child in children {
|
for child in children {
|
||||||
Self::layout_subtree(frame, tree, indent + 1, child, result);
|
self.layout_subtree(frame, tree, indent + 1, child, blocks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push_back(Block::after(indent, id.clone()))
|
// Trailing ghost cursor, for positioning according to last cursor line
|
||||||
|
if self.last_cursor.refers_to_last_child_of(id) {
|
||||||
|
let block = Block::new(frame, BlockId::LastCursor, Empty);
|
||||||
|
blocks.blocks_mut().push_back(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing editor or pseudomessage
|
||||||
|
if self.cursor.refers_to_last_child_of(id) {
|
||||||
|
// TODO Render proper editor or pseudocursor
|
||||||
|
let block = Block::new(frame, BlockId::Cursor, Text::new("TODO"));
|
||||||
|
blocks.blocks_mut().push_back(block);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_tree(frame: &mut Frame, tree: Tree<M>) -> Blocks<M::Id> {
|
fn layout_tree(&self, frame: &mut Frame, tree: Tree<M>) -> TreeBlocks<M::Id> {
|
||||||
let mut blocks = Blocks::new();
|
let root = Root::Tree(tree.root().clone());
|
||||||
Self::layout_subtree(frame, &tree, 0, tree.root(), &mut blocks);
|
let mut blocks = TreeBlocks::new(root.clone(), root);
|
||||||
blocks.roots = Some((tree.root().clone(), tree.root().clone()));
|
self.layout_subtree(frame, &tree, 0, tree.root(), &mut blocks);
|
||||||
blocks
|
blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a [`Blocks`] of the current cursor's immediate surroundings.
|
fn layout_bottom(&self, frame: &mut Frame) -> TreeBlocks<M::Id> {
|
||||||
async fn layout_cursor_surroundings(&self, frame: &mut Frame) -> Blocks<M::Id> {
|
let mut blocks = TreeBlocks::new(Root::Bottom, Root::Bottom);
|
||||||
let size = frame.size();
|
|
||||||
|
|
||||||
let cursor_path = self.cursor_path(&self.cursor).await;
|
// Ghost cursor, for positioning according to last cursor line
|
||||||
let last_cursor_path = self.cursor_path(&self.last_cursor).await;
|
if let Cursor::Editor(None) | Cursor::Pseudo(None) = self.last_cursor {
|
||||||
let tree_id = Self::cursor_tree_id(&self.cursor, &cursor_path);
|
let block = Block::new(frame, BlockId::LastCursor, Empty);
|
||||||
let cursor_line = Self::cursor_line(
|
blocks.blocks_mut().push_back(block);
|
||||||
&self.last_blocks,
|
|
||||||
&self.cursor,
|
|
||||||
&cursor_path,
|
|
||||||
&last_cursor_path,
|
|
||||||
size,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(tree_id) = tree_id {
|
|
||||||
let tree = self.store.tree(tree_id).await;
|
|
||||||
let mut blocks = Self::layout_tree(frame, tree);
|
|
||||||
blocks.recalculate_offsets(|b| {
|
|
||||||
if self.cursor.matches_block(b) {
|
|
||||||
Some(cursor_line)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
blocks
|
|
||||||
} else {
|
|
||||||
Blocks::new_bottom(cursor_line)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Editor or pseudomessage
|
||||||
|
if let Cursor::Editor(None) | Cursor::Pseudo(None) = self.cursor {
|
||||||
|
// TODO Render proper editor or pseudocursor
|
||||||
|
let block = Block::new(frame, BlockId::Cursor, Text::new("TODO"));
|
||||||
|
blocks.blocks_mut().push_back(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_so_cursor_is_visible(blocks: &mut Blocks<M::Id>, cursor: &Cursor<M::Id>, size: Size) {
|
async fn expand_to_top(&self, frame: &mut Frame, blocks: &mut TreeBlocks<M::Id>) {
|
||||||
if !matches!(cursor, Cursor::Msg(_)) {
|
|
||||||
// In all other cases, there is special scrolling behaviour, so
|
|
||||||
// let's not interfere.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let block = blocks
|
|
||||||
.find(|b| cursor.matches_block(b))
|
|
||||||
// This should never happen since we always start rendering the
|
|
||||||
// blocks from the cursor.
|
|
||||||
.expect("no cursor found");
|
|
||||||
|
|
||||||
let min_line = 0;
|
|
||||||
let max_line = size.height as i32 - block.height();
|
|
||||||
if block.line < min_line {
|
|
||||||
blocks.offset(min_line - block.line);
|
|
||||||
} else if block.line > max_line {
|
|
||||||
blocks.offset(max_line - block.line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to obtain a [`Cursor::Msg`] pointing to the block.
|
|
||||||
fn as_msg_cursor(block: &Block<M::Id>) -> Option<Cursor<M::Id>> {
|
|
||||||
match &block.body {
|
|
||||||
BlockBody::Msg(MsgBlock { id, .. }) => Some(Cursor::Msg(id.clone())),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_cursor_so_it_is_visible(
|
|
||||||
blocks: &mut Blocks<M::Id>,
|
|
||||||
cursor: &mut Cursor<M::Id>,
|
|
||||||
size: Size,
|
|
||||||
) {
|
|
||||||
if !matches!(cursor, Cursor::Msg(_)) {
|
|
||||||
// In all other cases, there is special scrolling behaviour, so
|
|
||||||
// let's not interfere.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let block = blocks
|
|
||||||
.find(|b| cursor.matches_block(b))
|
|
||||||
// This should never happen since we always start rendering the
|
|
||||||
// blocks from the cursor.
|
|
||||||
.expect("no cursor found");
|
|
||||||
|
|
||||||
let min_line = 1 - block.height();
|
|
||||||
let max_line = size.height as i32 - 1;
|
|
||||||
|
|
||||||
let new_cursor = if block.line < min_line {
|
|
||||||
// Move cursor to first possible visible block
|
|
||||||
blocks
|
|
||||||
.iter()
|
|
||||||
.filter(|b| b.line >= min_line)
|
|
||||||
.find_map(Self::as_msg_cursor)
|
|
||||||
} else if block.line > max_line {
|
|
||||||
// Move cursor to last possible visible block
|
|
||||||
blocks
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.filter(|b| b.line <= max_line)
|
|
||||||
.find_map(Self::as_msg_cursor)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(new_cursor) = new_cursor {
|
|
||||||
*cursor = new_cursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn expand_blocks_up(&self, frame: &mut Frame, blocks: &mut Blocks<M::Id>) {
|
|
||||||
while blocks.top_line > 0 {
|
|
||||||
let tree_id = if let Some((root_top, _)) = &blocks.roots {
|
|
||||||
self.store.prev_tree_id(root_top).await
|
|
||||||
} else {
|
|
||||||
self.store.last_tree_id().await
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(tree_id) = tree_id {
|
|
||||||
let tree = self.store.tree(&tree_id).await;
|
|
||||||
blocks.prepend(Self::layout_tree(frame, tree));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn expand_blocks_down(&self, frame: &mut Frame, blocks: &mut Blocks<M::Id>) {
|
|
||||||
while blocks.bottom_line < frame.size().height as i32 {
|
|
||||||
let tree_id = if let Some((_, root_bot)) = &blocks.roots {
|
|
||||||
self.store.next_tree_id(root_bot).await
|
|
||||||
} else {
|
|
||||||
// We assume that a Blocks without roots is at the bottom of the
|
|
||||||
// room's history. Therefore, there are no more messages below.
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(tree_id) = tree_id {
|
|
||||||
let tree = self.store.tree(&tree_id).await;
|
|
||||||
blocks.append(Self::layout_tree(frame, tree));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn clamp_scrolling(&self, frame: &mut Frame, blocks: &mut Blocks<M::Id>) {
|
|
||||||
let size = frame.size();
|
|
||||||
let top_line = 0;
|
let top_line = 0;
|
||||||
let bottom_line = size.height as i32 - 1;
|
|
||||||
|
|
||||||
self.expand_blocks_up(frame, blocks).await;
|
while blocks.blocks().top_line > top_line {
|
||||||
|
let top_root = blocks.top_root();
|
||||||
if blocks.top_line > top_line {
|
let prev_tree_id = match top_root {
|
||||||
blocks.offset(top_line - blocks.top_line);
|
Root::Bottom => self.store.last_tree_id().await,
|
||||||
|
Root::Tree(tree_id) => self.store.prev_tree_id(tree_id).await,
|
||||||
|
};
|
||||||
|
let prev_tree_id = match prev_tree_id {
|
||||||
|
Some(tree_id) => tree_id,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
let prev_tree = self.store.tree(&prev_tree_id).await;
|
||||||
|
blocks.prepend(self.layout_tree(frame, prev_tree));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.expand_blocks_down(frame, blocks).await;
|
|
||||||
|
|
||||||
if blocks.bottom_line < bottom_line {
|
|
||||||
blocks.offset(bottom_line - blocks.bottom_line);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.expand_blocks_up(frame, blocks).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn relayout(&mut self, frame: &mut Frame) {
|
async fn expand_to_bottom(&self, frame: &mut Frame, blocks: &mut TreeBlocks<M::Id>) {
|
||||||
let size = frame.size();
|
let bottom_line = frame.size().height as i32 - 1;
|
||||||
|
|
||||||
let mut blocks = self.layout_cursor_surroundings(frame).await;
|
while blocks.blocks().bottom_line < bottom_line {
|
||||||
|
let bottom_root = blocks.bottom_root();
|
||||||
|
let next_tree_id = match bottom_root {
|
||||||
|
Root::Bottom => break,
|
||||||
|
Root::Tree(tree_id) => self.store.prev_tree_id(tree_id).await,
|
||||||
|
};
|
||||||
|
if let Some(next_tree_id) = next_tree_id {
|
||||||
|
let next_tree = self.store.tree(&next_tree_id).await;
|
||||||
|
blocks.append(self.layout_tree(frame, next_tree));
|
||||||
|
} else {
|
||||||
|
blocks.append(self.layout_bottom(frame));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fill_screen_and_clamp_scrolling(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame,
|
||||||
|
blocks: &mut TreeBlocks<M::Id>,
|
||||||
|
) {
|
||||||
|
let top_line = 0;
|
||||||
|
let bottom_line = frame.size().height as i32 - 1;
|
||||||
|
|
||||||
|
self.expand_to_top(frame, blocks).await;
|
||||||
|
|
||||||
|
if blocks.blocks().top_line > top_line {
|
||||||
|
blocks.blocks_mut().set_top_line(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.expand_to_bottom(frame, blocks).await;
|
||||||
|
|
||||||
|
if blocks.blocks().bottom_line < bottom_line {
|
||||||
|
blocks.blocks_mut().set_bottom_line(bottom_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.expand_to_top(frame, blocks).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn layout_last_cursor_seed(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame,
|
||||||
|
last_cursor_path: &Path<M::Id>,
|
||||||
|
) -> TreeBlocks<M::Id> {
|
||||||
|
match &self.last_cursor {
|
||||||
|
Cursor::Bottom => {
|
||||||
|
let mut blocks = self.layout_bottom(frame);
|
||||||
|
|
||||||
|
let bottom_line = frame.size().height as i32 - 1;
|
||||||
|
blocks.blocks_mut().set_bottom_line(bottom_line);
|
||||||
|
|
||||||
|
blocks
|
||||||
|
}
|
||||||
|
Cursor::Editor(None) | Cursor::Pseudo(None) => {
|
||||||
|
let mut blocks = self.layout_bottom(frame);
|
||||||
|
|
||||||
|
blocks
|
||||||
|
.blocks_mut()
|
||||||
|
.recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line);
|
||||||
|
|
||||||
|
blocks
|
||||||
|
}
|
||||||
|
Cursor::Msg(_) | Cursor::Editor(Some(_)) | Cursor::Pseudo(Some(_)) => {
|
||||||
|
let root = last_cursor_path.first();
|
||||||
|
let tree = self.store.tree(root).await;
|
||||||
|
let mut blocks = self.layout_tree(frame, tree);
|
||||||
|
|
||||||
|
blocks
|
||||||
|
.blocks_mut()
|
||||||
|
.recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line);
|
||||||
|
|
||||||
|
blocks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn layout_cursor_seed(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame,
|
||||||
|
last_cursor_path: &Path<M::Id>,
|
||||||
|
cursor_path: &Path<M::Id>,
|
||||||
|
) -> TreeBlocks<M::Id> {
|
||||||
|
let bottom_line = frame.size().height as i32 - 1;
|
||||||
|
|
||||||
|
match &self.cursor {
|
||||||
|
Cursor::Bottom | Cursor::Editor(None) | Cursor::Pseudo(None) => {
|
||||||
|
let mut blocks = self.layout_bottom(frame);
|
||||||
|
|
||||||
|
blocks.blocks_mut().set_bottom_line(bottom_line);
|
||||||
|
|
||||||
|
blocks
|
||||||
|
}
|
||||||
|
Cursor::Msg(_) | Cursor::Editor(Some(_)) | Cursor::Pseudo(Some(_)) => {
|
||||||
|
let root = cursor_path.first();
|
||||||
|
let tree = self.store.tree(root).await;
|
||||||
|
let mut blocks = self.layout_tree(frame, tree);
|
||||||
|
|
||||||
|
let cursor_above_last = cursor_path < last_cursor_path;
|
||||||
|
let cursor_line = if cursor_above_last { 0 } else { bottom_line };
|
||||||
|
blocks
|
||||||
|
.blocks_mut()
|
||||||
|
.recalculate_offsets(&BlockId::from_cursor(&self.cursor), cursor_line);
|
||||||
|
|
||||||
|
blocks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn layout_initial_seed(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame,
|
||||||
|
last_cursor_path: &Path<M::Id>,
|
||||||
|
cursor_path: &Path<M::Id>,
|
||||||
|
) -> TreeBlocks<M::Id> {
|
||||||
|
if let Cursor::Bottom = self.cursor {
|
||||||
|
self.layout_cursor_seed(frame, last_cursor_path, cursor_path)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
self.layout_last_cursor_seed(frame, last_cursor_path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn relayout(&mut self, frame: &mut Frame) -> TreeBlocks<M::Id> {
|
||||||
|
// The basic idea is this:
|
||||||
|
//
|
||||||
|
// First, layout a full screen of blocks around self.last_cursor, using
|
||||||
|
// self.last_cursor_line for offset positioning.
|
||||||
|
//
|
||||||
|
// Then, check if self.cursor is somewhere in these blocks. If it is, we
|
||||||
|
// now know the position of our own cursor. If it is not, it has jumped
|
||||||
|
// too far away from self.last_cursor and we'll need to render a new
|
||||||
|
// full screen of blocks around self.cursor before proceeding, using the
|
||||||
|
// cursor paths to determine the position of self.cursor on the screen.
|
||||||
|
//
|
||||||
|
// Now that we have a more-or-less accurate screen position of
|
||||||
|
// self.cursor, we can perform the actual cursor logic, i.e. make the
|
||||||
|
// cursor visible or move it so it is visible.
|
||||||
|
//
|
||||||
|
// This entire process is complicated by the different kinds of cursors.
|
||||||
|
|
||||||
|
let last_cursor_path = self.cursor_path(&self.last_cursor).await;
|
||||||
|
let cursor_path = self.cursor_path(&self.cursor).await;
|
||||||
|
|
||||||
|
let mut blocks = self
|
||||||
|
.layout_initial_seed(frame, &last_cursor_path, &cursor_path)
|
||||||
|
.await;
|
||||||
|
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if !self.contains_cursor(&blocks) {
|
||||||
|
blocks = self
|
||||||
|
.layout_cursor_seed(frame, &last_cursor_path, &cursor_path)
|
||||||
|
.await;
|
||||||
|
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
if self.make_cursor_visible {
|
if self.make_cursor_visible {
|
||||||
Self::scroll_so_cursor_is_visible(&mut blocks, &self.cursor, size);
|
// self.make_cursor_visible(&mut blocks).await; // TODO
|
||||||
|
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
// self.move_cursor_so_it_is_visible(&mut blocks); // TODO
|
||||||
|
self.fill_screen_and_clamp_scrolling(frame, &mut blocks)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clamp_scrolling(frame, &mut blocks).await;
|
|
||||||
|
|
||||||
if !self.make_cursor_visible {
|
|
||||||
Self::move_cursor_so_it_is_visible(&mut blocks, &mut self.cursor, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.last_blocks = blocks;
|
|
||||||
self.last_cursor = self.cursor.clone();
|
self.last_cursor = self.cursor.clone();
|
||||||
|
self.last_cursor_line = self.cursor_line(&blocks);
|
||||||
self.make_cursor_visible = false;
|
self.make_cursor_visible = false;
|
||||||
|
|
||||||
|
blocks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use crossterm::style::{ContentStyle, Stylize};
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
use time::format_description::FormatItem;
|
use time::format_description::FormatItem;
|
||||||
use time::macros::format_description;
|
use time::macros::format_description;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::euph::api::Time;
|
|
||||||
use crate::ui::widgets::background::Background;
|
use crate::ui::widgets::background::Background;
|
||||||
use crate::ui::widgets::text::Text;
|
use crate::ui::widgets::text::Text;
|
||||||
use crate::ui::widgets::BoxedWidget;
|
use crate::ui::widgets::BoxedWidget;
|
||||||
|
|
@ -18,9 +18,9 @@ fn style_inverted() -> ContentStyle {
|
||||||
ContentStyle::default().black().on_white()
|
ContentStyle::default().black().on_white()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widget(time: Option<Time>, highlighted: bool) -> BoxedWidget {
|
pub fn widget(time: Option<OffsetDateTime>, highlighted: bool) -> BoxedWidget {
|
||||||
let text = if let Some(time) = time {
|
let text = if let Some(time) = time {
|
||||||
time.0.format(TIME_FORMAT).expect("could not format time")
|
time.format(TIME_FORMAT).expect("could not format time")
|
||||||
} else {
|
} else {
|
||||||
TIME_EMPTY.to_string()
|
TIME_EMPTY.to_string()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
88
src/ui/chat/tree/tree_blocks.rs
Normal file
88
src/ui/chat/tree/tree_blocks.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
use crate::ui::chat::blocks::Blocks;
|
||||||
|
|
||||||
|
use super::Cursor;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BlockId<I> {
|
||||||
|
Msg(I),
|
||||||
|
Cursor,
|
||||||
|
LastCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Clone> BlockId<I> {
|
||||||
|
pub fn from_cursor(cursor: &Cursor<I>) -> Self {
|
||||||
|
match cursor {
|
||||||
|
Cursor::Msg(id) => Self::Msg(id.clone()),
|
||||||
|
_ => Self::Cursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Root<I> {
|
||||||
|
Bottom,
|
||||||
|
Tree(I),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TreeBlocks<I> {
|
||||||
|
blocks: Blocks<BlockId<I>>,
|
||||||
|
top_root: Root<I>,
|
||||||
|
bottom_root: Root<I>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> TreeBlocks<I> {
|
||||||
|
pub fn new(top_root: Root<I>, bottom_root: Root<I>) -> Self {
|
||||||
|
Self {
|
||||||
|
blocks: Blocks::new(),
|
||||||
|
top_root,
|
||||||
|
bottom_root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See [`Blocks::new_below`].
|
||||||
|
pub fn new_below(line: i32, top_root: Root<I>, bottom_root: Root<I>) -> Self {
|
||||||
|
Self {
|
||||||
|
blocks: Blocks::new_below(line),
|
||||||
|
top_root,
|
||||||
|
bottom_root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blocks(&self) -> &Blocks<BlockId<I>> {
|
||||||
|
&self.blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blocks_mut(&mut self) -> &mut Blocks<BlockId<I>> {
|
||||||
|
&mut self.blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_blocks(self) -> Blocks<BlockId<I>> {
|
||||||
|
self.blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn top_root(&self) -> &Root<I> {
|
||||||
|
&self.top_root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn top_root_mut(&mut self) -> &mut Root<I> {
|
||||||
|
&mut self.top_root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottom_root(&self) -> &Root<I> {
|
||||||
|
&self.bottom_root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottom_root_mut(&mut self) -> &mut Root<I> {
|
||||||
|
&mut self.bottom_root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepend(&mut self, other: Self) {
|
||||||
|
self.blocks.prepend(other.blocks);
|
||||||
|
self.top_root = other.top_root;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&mut self, other: Self) {
|
||||||
|
self.blocks.append(other.blocks);
|
||||||
|
self.bottom_root = other.bottom_root;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/ui/chat/tree/widgets.rs
Normal file
24
src/ui/chat/tree/widgets.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use crate::store::Msg;
|
||||||
|
use crate::ui::widgets::join::{HJoin, Segment};
|
||||||
|
use crate::ui::widgets::padding::Padding;
|
||||||
|
use crate::ui::widgets::text::Text;
|
||||||
|
use crate::ui::widgets::BoxedWidget;
|
||||||
|
|
||||||
|
use super::time;
|
||||||
|
|
||||||
|
pub fn msg<M: Msg>(highlighted: bool, indent: usize, msg: &M) -> BoxedWidget {
|
||||||
|
HJoin::new(vec![
|
||||||
|
Segment::new(Padding::new(time::widget(Some(msg.time()), highlighted)).right(1)),
|
||||||
|
Segment::new(Padding::new(Text::new(msg.nick())).right(1)),
|
||||||
|
Segment::new(Text::new(msg.content()).wrap(true)),
|
||||||
|
])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msg_placeholder(highlighted: bool, indent: usize) -> BoxedWidget {
|
||||||
|
HJoin::new(vec![
|
||||||
|
Segment::new(Padding::new(time::widget(None, highlighted)).right(1)),
|
||||||
|
Segment::new(Text::new("[...]")),
|
||||||
|
])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,8 @@ impl Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap(mut self) -> Self {
|
pub fn wrap(mut self, active: bool) -> Self {
|
||||||
self.wrap = true;
|
self.wrap = active;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue