From 7da5ba04a5b48fb2c40b6d2ba39a5dccb79b95ad Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 19 Jul 2022 21:16:39 +0200 Subject: [PATCH] Implement remaining layouting logic --- src/ui/chat.rs | 2 +- src/ui/chat/tree.rs | 21 ++-- src/ui/chat/tree/blocks.rs | 15 ++- src/ui/chat/tree/layout.rs | 192 ++++++++++++++++++++++--------------- 4 files changed, 141 insertions(+), 89 deletions(-) diff --git a/src/ui/chat.rs b/src/ui/chat.rs index 36b9b3c..f900ff6 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -126,7 +126,7 @@ pub enum Chat> { impl Widget for Chat where M: Msg, - M::Id: Send, + M::Id: Send + Sync, S: MsgStore + Send + Sync, { fn size(&self, frame: &mut Frame, max_width: Option, max_height: Option) -> Size { diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index 7aedf22..ed31ca7 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -22,11 +22,13 @@ use self::blocks::Blocks; /// Position of a cursor that is displayed as the last child of its parent /// message, or last thread if it has no parent. +#[derive(Debug, Clone, Copy)] struct LastChild { coming_from: Option, after: Option, } +#[derive(Debug, Clone, Copy)] enum Cursor { /// No cursor visible because it is at the bottom of the chat history. /// @@ -50,6 +52,10 @@ struct InnerTreeViewState> { last_blocks: Blocks, last_cursor: Cursor, cursor: Cursor, + /// Set to true if the chat should be scrolled such that the cursor is fully + /// visible (if possible). If set to false, then the cursor itself is moved + /// to a different message such that it remains visible. + make_cursor_visible: bool, editor: (), // TODO } @@ -60,6 +66,7 @@ impl> InnerTreeViewState { last_blocks: Blocks::new(), last_cursor: Cursor::Bottom, cursor: Cursor::Bottom, + make_cursor_visible: false, editor: (), } } @@ -87,7 +94,7 @@ pub struct TreeView>(Arc>> impl Widget for TreeView where M: Msg, - M::Id: Send, + M::Id: Send + Sync, S: MsgStore + Send + Sync, { fn size(&self, _frame: &mut Frame, _max_width: Option, _max_height: Option) -> Size { @@ -95,16 +102,8 @@ where } async fn render(self: Box, frame: &mut Frame) { - // Determine current cursor position - // If cursor in last blocks, use that - // If cursor below last cursor, use last line - // Otherwise, use first line - // Layout starting from cursor tree - // Make cursor visible - // If cursor was moved last, scroll so it is fully visible - // Otherwise, move cursor so it is barely visible - // Clamp scrolling and fill screen again - // Update last layout and last cursor position + let mut guard = self.0.lock().await; + guard.relayout(frame).await; // Draw layout to screen todo!() } diff --git a/src/ui/chat/tree/blocks.rs b/src/ui/chat/tree/blocks.rs index 0c6f1aa..0175545 100644 --- a/src/ui/chat/tree/blocks.rs +++ b/src/ui/chat/tree/blocks.rs @@ -1,22 +1,25 @@ //! Intermediate representation of chat history as blocks of things. -use std::collections::VecDeque; +use std::collections::{vec_deque, VecDeque}; use chrono::{DateTime, Utc}; use toss::styled::Styled; use crate::macros::some_or_return; +#[derive(Debug, Clone, Copy)] pub enum MarkerBlock { - After(I), + After(I), // TODO Is this marker necessary? Bottom, } +#[derive(Debug, Clone)] pub enum MsgContent { Msg { nick: Styled, lines: Vec }, Placeholder, } +#[derive(Debug, Clone)] pub struct MsgBlock { pub id: I, pub content: MsgContent, @@ -31,16 +34,19 @@ impl MsgBlock { } } +#[derive(Debug, Clone, Copy)] pub struct ComposeBlock { // TODO Editor widget } +#[derive(Debug, Clone)] pub enum BlockBody { Marker(MarkerBlock), Msg(MsgBlock), Compose(ComposeBlock), } +#[derive(Debug, Clone)] pub struct Block { pub line: i32, pub time: Option>, @@ -121,6 +127,7 @@ impl Block { /// equation simplifies to /// /// `bottom_line = top_line - 1` +#[derive(Debug, Clone)] pub struct Blocks { pub blocks: VecDeque>, /// The top line of the first block. Useful for prepending blocks, @@ -157,6 +164,10 @@ impl Blocks { self.blocks.iter().find(|b| f(b)) } + pub fn iter(&self) -> vec_deque::Iter> { + self.blocks.iter() + } + pub fn update(&mut self, f: F) where F: Fn(&mut Block), diff --git a/src/ui/chat/tree/layout.rs b/src/ui/chat/tree/layout.rs index 37f2a90..3700c38 100644 --- a/src/ui/chat/tree/layout.rs +++ b/src/ui/chat/tree/layout.rs @@ -4,80 +4,9 @@ use toss::frame::{Frame, Size}; use crate::store::{Msg, MsgStore, Path, Tree}; -use super::blocks::{Block, BlockBody, Blocks, MarkerBlock}; +use super::blocks::{Block, BlockBody, Blocks, MarkerBlock, MsgBlock}; use super::{util, Cursor, InnerTreeViewState}; -/* -impl TreeView { - // TODO Split up based on cursor presence - pub async fn layout_blocks>( - &mut self, - store: &S, - cursor: Option<&Cursor>, - frame: &mut Frame, - size: Size, - ) -> Blocks { - if let Some(cursor) = cursor { - // TODO Ensure focus lies on cursor path, otherwise unfocus - // TODO Unfold all messages on path to cursor - - // Layout cursor subtree (with correct offsets based on cursor) - let cursor_path = store.path(&cursor.id).await; - let cursor_tree_id = cursor_path.first(); - let cursor_tree = store.tree(cursor_tree_id).await; - let mut blocks = layout_tree(frame, size, cursor_tree); - blocks.calculate_offsets_with_cursor(cursor, size.height); - - // Expand upwards and downwards, ensuring the blocks are not - // scrolled too far in any direction. - // - // If the blocks fill the screen, scrolling stops when the topmost - // message is at the top of the screen or the bottommost message is - // at the bottom. If they don't fill the screen, the bottommost - // message should always be at the bottom. - // - // Because our helper functions always expand the blocks until they - // reach the top or bottom of the screen, we can determine that - // we're at the top/bottom if expansion stopped anywhere in the - // middle of the screen. - // - // TODO Don't expand if there is a focus - let mut top_tree_id = Some(cursor_tree_id.clone()); - Self::expand_blocks_up(store, frame, size, &mut blocks, &mut top_tree_id).await; - if blocks.top_line > 0 { - blocks.offset(-blocks.top_line); - } - let mut bot_tree_id = Some(cursor_tree_id.clone()); - Self::expand_blocks_down(store, frame, size, &mut blocks, &mut bot_tree_id).await; - if blocks.bottom_line < size.height as i32 - 1 { - blocks.offset(size.height as i32 - 1 - blocks.bottom_line); - } - // If we only moved the blocks down, we need to expand upwards again - // to make sure we fill the screen. - Self::expand_blocks_up(store, frame, size, &mut blocks, &mut top_tree_id).await; - - blocks - } else { - // TODO Ensure there is no focus - - // Start at the bottom of the screen - let mut blocks = Blocks::new_below(size.height as i32 - 1); - - // Expand upwards from last tree - if let Some(last_tree_id) = store.last_tree().await { - let last_tree = store.tree(&last_tree_id).await; - blocks.prepend(layout_tree(frame, size, last_tree)); - - let mut tree_id = Some(last_tree_id); - Self::expand_blocks_up(store, frame, size, &mut blocks, &mut tree_id).await; - } - - blocks - } - } -} -*/ - impl Cursor { fn matches_block(&self, block: &Block) -> bool { match self { @@ -192,7 +121,7 @@ impl> InnerTreeViewState { } /// Create a [`Blocks`] of the current cursor's immediate surroundings. - pub async fn layout_cursor_surroundings(&self, frame: &mut Frame) -> Blocks { + async fn layout_cursor_surroundings(&self, frame: &mut Frame) -> Blocks { let size = frame.size(); let cursor_path = self.cursor_path(&self.cursor).await; @@ -224,7 +153,80 @@ impl> InnerTreeViewState { } } - pub async fn expand_blocks_up(&self, frame: &mut Frame, blocks: &mut Blocks) { + fn scroll_so_cursor_is_visible(blocks: &mut Blocks, cursor: &Cursor, size: Size) { + if let Some(block) = blocks.find(|b| cursor.matches_block(b)) { + 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); + } + } else { + // This should never happen since we always start rendering the + // blocks from the cursor. + panic!("no cursor found"); + } + } + + /// Try to obtain a normal cursor (i.e. no composing or placeholder cursor) + /// pointing to the block. + fn as_direct_cursor(block: &Block) -> Option> { + match &block.body { + BlockBody::Marker(MarkerBlock::Bottom) => Some(Cursor::Bottom), + BlockBody::Msg(MsgBlock { id, .. }) => Some(Cursor::Msg(id.clone())), + _ => None, + } + } + + fn move_cursor_so_it_is_visible( + blocks: &mut Blocks, + cursor: &mut Cursor, + size: Size, + ) { + if matches!(cursor, Cursor::Compose(_) | Cursor::Placeholder(_)) { + // In this case, we can't easily move the cursor since moving it + // would change how the entire layout is rendered in + // difficult-to-predict ways. + // + // Also, the user has initiated a reply to get into this state. This + // confirms that they want their cursor in precisely its current + // place. Moving it might lead to mis-replies and frustration. + return; + } + + if let Some(block) = blocks.find(|b| cursor.matches_block(b)) { + 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_direct_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_direct_cursor) + } else { + None + }; + + if let Some(new_cursor) = new_cursor { + *cursor = new_cursor; + } + } else { + // This should never happen since we always start rendering the + // blocks from the cursor. + panic!("no cursor found"); + } + } + + async fn expand_blocks_up(&self, frame: &mut Frame, blocks: &mut Blocks) { while blocks.top_line > 0 { let tree_id = if let Some((root_top, _)) = &blocks.roots { self.store.prev_tree(root_top).await @@ -241,7 +243,7 @@ impl> InnerTreeViewState { } } - pub async fn expand_blocks_down(&self, frame: &mut Frame, blocks: &mut Blocks) { + async fn expand_blocks_down(&self, frame: &mut Frame, blocks: &mut Blocks) { while blocks.bottom_line < frame.size().height as i32 { let tree_id = if let Some((_, root_bot)) = &blocks.roots { self.store.next_tree(root_bot).await @@ -259,4 +261,44 @@ impl> InnerTreeViewState { } } } + + async fn clamp_scrolling(&self, frame: &mut Frame, blocks: &mut Blocks) { + let size = frame.size(); + let top_line = 0; + let bottom_line = size.height as i32 - 1; + + self.expand_blocks_up(frame, blocks).await; + + if blocks.top_line > top_line { + blocks.offset(top_line - blocks.top_line); + } + + 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) { + let size = frame.size(); + + let mut blocks = self.layout_cursor_surroundings(frame).await; + + if self.make_cursor_visible { + Self::scroll_so_cursor_is_visible(&mut blocks, &self.cursor, size); + } + + 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.make_cursor_visible = false; + } }