Port most of cursor movement logic

This commit is contained in:
Joscha 2022-07-20 14:07:02 +02:00
parent 4cf0d861db
commit 48943f2316
4 changed files with 94 additions and 254 deletions

View file

@ -19,23 +19,6 @@ pub enum Mode {
// Flat, // Flat,
} }
pub struct Cursor<I> {
id: I,
/// Where on the screen the cursor is visible (`0.0` = first line, `1.0` =
/// last line).
proportion: f32,
}
impl<I> Cursor<I> {
/// Create a new cursor with arbitrary proportion.
pub fn new(id: I) -> Self {
Self {
id,
proportion: 0.0,
}
}
}
pub struct ChatState<M: Msg, S: MsgStore<M>> { pub struct ChatState<M: Msg, S: MsgStore<M>> {
store: S, store: S,
mode: Mode, mode: Mode,

View file

@ -1,6 +1,6 @@
// mod action; // mod action;
mod blocks; mod blocks;
// mod cursor; mod cursor;
mod layout; mod layout;
mod render; mod render;
mod util; mod util;
@ -14,54 +14,13 @@ use toss::frame::{Frame, Size};
use crate::store::{Msg, MsgStore}; use crate::store::{Msg, MsgStore};
use crate::ui::widgets::Widget; use crate::ui::widgets::Widget;
use self::blocks::{Block, BlockBody, Blocks, MarkerBlock}; use self::blocks::Blocks;
use self::cursor::Cursor;
/////////// ///////////
// State // // State //
/////////// ///////////
/// 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<I> {
coming_from: Option<I>,
after: Option<I>,
}
#[derive(Debug, Clone, Copy)]
enum Cursor<I> {
/// No cursor visible because it is at the bottom of the chat history.
///
/// See also [`Anchor::Bottom`].
Bottom,
/// The cursor points to a message.
Msg(I),
/// The cursor has turned into an editor because we're composing a new
/// message.
Compose(LastChild<I>),
/// A placeholder message is being displayed for a message that was just
/// sent by the user.
///
/// Will be replaced by a [`Cursor::Msg`] as soon as the server replies to
/// the send command with the sent message.
Placeholder(LastChild<I>),
}
impl<I: Eq> Cursor<I> {
fn matches_block(&self, block: &Block<I>) -> bool {
match self {
Self::Bottom => matches!(&block.body, BlockBody::Marker(MarkerBlock::Bottom)),
Self::Msg(id) => matches!(&block.body, BlockBody::Msg(msg) if msg.id == *id),
Self::Compose(lc) | Self::Placeholder(lc) => match &lc.after {
Some(bid) => {
matches!(&block.body, BlockBody::Marker(MarkerBlock::After(aid)) if aid == bid)
}
None => matches!(&block.body, BlockBody::Marker(MarkerBlock::Bottom)),
},
}
}
}
struct InnerTreeViewState<M: Msg, S: MsgStore<M>> { struct InnerTreeViewState<M: Msg, S: MsgStore<M>> {
store: S, store: S,
last_blocks: Blocks<M::Id>, last_blocks: Blocks<M::Id>,

View file

@ -1,71 +1,53 @@
//! Moving the cursor around. //! Moving the cursor around.
use toss::frame::{Frame, Size};
use crate::store::{Msg, MsgStore, Tree}; use crate::store::{Msg, MsgStore, Tree};
use super::blocks::Blocks; use super::blocks::{Block, BlockBody, MarkerBlock};
use super::{util, Cursor, TreeView}; use super::InnerTreeViewState;
impl<M: Msg> TreeView<M> { /// Position of a cursor that is displayed as the last child of its parent
#[allow(clippy::too_many_arguments)] /// message, or last thread if it has no parent.
async fn correct_cursor_offset<S: MsgStore<M>>( #[derive(Debug, Clone, Copy)]
&mut self, pub struct LastChild<I> {
store: &S, pub coming_from: Option<I>,
frame: &mut Frame, pub after: Option<I>,
size: Size,
old_blocks: &Blocks<M::Id>,
old_cursor_id: &Option<M::Id>,
cursor: &mut Cursor<M::Id>,
) {
if let Some(block) = old_blocks.find(&cursor.id) {
// The cursor is still visible in the old blocks, so we just need to
// adjust the proportion such that the blocks stay still.
cursor.proportion = util::line_to_proportion(size.height, block.line);
} else if let Some(old_cursor_id) = old_cursor_id {
// The cursor is not visible any more. However, we can estimate
// whether it is above or below the previous cursor position by
// lexicographically comparing both positions' paths.
let old_path = store.path(old_cursor_id).await;
let new_path = store.path(&cursor.id).await;
if new_path < old_path {
// Because we moved upwards, the cursor should appear at the top
// of the screen.
cursor.proportion = 0.0;
} else {
// Because we moved downwards, the cursor should appear at the
// bottom of the screen.
cursor.proportion = 1.0;
}
} else {
// We were scrolled all the way to the bottom, so the cursor must
// have been offscreen somewhere above.
cursor.proportion = 0.0;
} }
// The cursor should be visible in its entirety on the screen now. If it #[derive(Debug, Clone, Copy)]
// isn't, we need to scroll the screen such that the cursor becomes fully pub enum Cursor<I> {
// visible again. To do this, we'll need to re-layout because the cursor /// No cursor visible because it is at the bottom of the chat history.
// could've moved anywhere. ///
let blocks = self.layout_blocks(store, Some(cursor), frame, size).await; /// See also [`Anchor::Bottom`].
let cursor_block = blocks.find(&cursor.id).expect("cursor must be in blocks"); Bottom,
// First, ensure the cursor's last line is not below the bottom of the /// The cursor points to a message.
// screen. Then, ensure its top line is not above the top of the screen. Msg(I),
// If the cursor has more lines than the screen, the user should still /// The cursor has turned into an editor because we're composing a new
// see the top of the cursor so they can start reading its contents. /// message.
let min_line = 0; Compose(LastChild<I>),
let max_line = size.height as i32 - cursor_block.height; /// A placeholder message is being displayed for a message that was just
// Not using clamp because it is possible that max_line < min_line /// sent by the user.
let cursor_line = cursor_block.line.min(max_line).max(min_line); ///
cursor.proportion = util::line_to_proportion(size.height, cursor_line); /// Will be replaced by a [`Cursor::Msg`] as soon as the server replies to
/// the send command with the sent message.
// There is no need to ensure the screen is not scrolled too far up or Placeholder(LastChild<I>),
// down. The messages in `blocks` are already scrolled correctly and
// this function will not scroll the wrong way. If the cursor moves too
// far up, the screen will only scroll down, not further up. The same
// goes for the other direction.
} }
impl<I: Eq> Cursor<I> {
pub fn matches_block(&self, block: &Block<I>) -> bool {
match self {
Self::Bottom => matches!(&block.body, BlockBody::Marker(MarkerBlock::Bottom)),
Self::Msg(id) => matches!(&block.body, BlockBody::Msg(msg) if msg.id == *id),
Self::Compose(lc) | Self::Placeholder(lc) => match &lc.after {
Some(bid) => {
matches!(&block.body, BlockBody::Marker(MarkerBlock::After(aid)) if aid == bid)
}
None => matches!(&block.body, BlockBody::Marker(MarkerBlock::Bottom)),
},
}
}
}
impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
fn find_parent(tree: &Tree<M>, id: &mut M::Id) -> bool { fn find_parent(tree: &Tree<M>, id: &mut M::Id) -> bool {
if let Some(parent) = tree.parent(id) { if let Some(parent) = tree.parent(id) {
*id = parent; *id = parent;
@ -96,20 +78,15 @@ impl<M: Msg> TreeView<M> {
/// Move to the previous sibling, or don't move if this is not possible. /// Move to the previous sibling, or don't move if this is not possible.
/// ///
/// Always stays at the same level of indentation. /// Always stays at the same level of indentation.
async fn find_prev_sibling<S: MsgStore<M>>( async fn find_prev_sibling(&self, tree: &mut Tree<M>, id: &mut M::Id) -> bool {
&self,
store: &S,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> bool {
if let Some(prev_sibling) = tree.prev_sibling(id) { if let Some(prev_sibling) = tree.prev_sibling(id) {
*id = prev_sibling; *id = prev_sibling;
true true
} else if tree.parent(id).is_none() { } else if tree.parent(id).is_none() {
// We're at the root of our tree, so we need to move to the root of // We're at the root of our tree, so we need to move to the root of
// the previous tree. // the previous tree.
if let Some(prev_tree_id) = store.prev_tree(tree.root()).await { if let Some(prev_tree_id) = self.store.prev_tree(tree.root()).await {
*tree = store.tree(&prev_tree_id).await; *tree = self.store.tree(&prev_tree_id).await;
*id = prev_tree_id; *id = prev_tree_id;
true true
} else { } else {
@ -123,20 +100,15 @@ impl<M: Msg> TreeView<M> {
/// Move to the next sibling, or don't move if this is not possible. /// Move to the next sibling, or don't move if this is not possible.
/// ///
/// Always stays at the same level of indentation. /// Always stays at the same level of indentation.
async fn find_next_sibling<S: MsgStore<M>>( async fn find_next_sibling(&self, tree: &mut Tree<M>, id: &mut M::Id) -> bool {
&self,
store: &S,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> bool {
if let Some(next_sibling) = tree.next_sibling(id) { if let Some(next_sibling) = tree.next_sibling(id) {
*id = next_sibling; *id = next_sibling;
true true
} else if tree.parent(id).is_none() { } else if tree.parent(id).is_none() {
// We're at the root of our tree, so we need to move to the root of // We're at the root of our tree, so we need to move to the root of
// the next tree. // the next tree.
if let Some(next_tree_id) = store.next_tree(tree.root()).await { if let Some(next_tree_id) = self.store.next_tree(tree.root()).await {
*tree = store.tree(&next_tree_id).await; *tree = self.store.tree(&next_tree_id).await;
*id = next_tree_id; *id = next_tree_id;
true true
} else { } else {
@ -148,15 +120,10 @@ impl<M: Msg> TreeView<M> {
} }
/// Move to the previous message, or don't move if this is not possible. /// Move to the previous message, or don't move if this is not possible.
async fn find_prev_msg<S: MsgStore<M>>( async fn find_prev_msg(&self, tree: &mut Tree<M>, id: &mut M::Id) -> bool {
&self,
store: &S,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> bool {
// Move to previous sibling, then to its last child // Move to previous sibling, then to its last child
// If not possible, move to parent // If not possible, move to parent
if self.find_prev_sibling(store, tree, id).await { if self.find_prev_sibling(tree, id).await {
while Self::find_last_child(tree, id) {} while Self::find_last_child(tree, id) {}
true true
} else { } else {
@ -165,17 +132,12 @@ impl<M: Msg> TreeView<M> {
} }
/// Move to the next message, or don't move if this is not possible. /// Move to the next message, or don't move if this is not possible.
async fn find_next_msg<S: MsgStore<M>>( async fn find_next_msg(&self, tree: &mut Tree<M>, id: &mut M::Id) -> bool {
&self,
store: &S,
tree: &mut Tree<M>,
id: &mut M::Id,
) -> bool {
if Self::find_first_child(tree, id) { if Self::find_first_child(tree, id) {
return true; return true;
} }
if self.find_next_sibling(store, tree, id).await { if self.find_next_sibling(tree, id).await {
return true; return true;
} }
@ -183,7 +145,7 @@ impl<M: Msg> TreeView<M> {
// can be found. // can be found.
let mut tmp_id = id.clone(); let mut tmp_id = id.clone();
while Self::find_parent(tree, &mut tmp_id) { while Self::find_parent(tree, &mut tmp_id) {
if self.find_next_sibling(store, tree, &mut tmp_id).await { if self.find_next_sibling(tree, &mut tmp_id).await {
*id = tmp_id; *id = tmp_id;
return true; return true;
} }
@ -192,65 +154,47 @@ impl<M: Msg> TreeView<M> {
false false
} }
pub async fn move_up<S: MsgStore<M>>( pub async fn move_up(&mut self, cursor: &mut Cursor<M::Id>) {
&mut self, match cursor {
store: &S, Cursor::Bottom => {
cursor: &mut Option<Cursor<M::Id>>, if let Some(last_tree_id) = self.store.last_tree().await {
frame: &mut Frame, let tree = self.store.tree(&last_tree_id).await;
size: Size, let mut id = last_tree_id;
) {
let old_blocks = self
.layout_blocks(store, cursor.as_ref(), frame, size)
.await;
let old_cursor_id = cursor.as_ref().map(|c| c.id.clone());
if let Some(cursor) = cursor {
// We have a cursor to move around
let path = store.path(&cursor.id).await;
let mut tree = store.tree(path.first()).await;
self.find_prev_msg(store, &mut tree, &mut cursor.id).await;
} else if let Some(last_tree) = store.last_tree().await {
// We need to select the last message of the last tree
let tree = store.tree(&last_tree).await;
let mut id = last_tree;
while Self::find_last_child(&tree, &mut id) {} while Self::find_last_child(&tree, &mut id) {}
*cursor = Some(Cursor::new(id)); *cursor = Cursor::Msg(id);
} }
// If neither condition holds, we can't set a cursor because there's no }
// message to move to. Cursor::Msg(ref mut msg) => {
let path = self.store.path(msg).await;
if let Some(cursor) = cursor { let mut tree = self.store.tree(path.first()).await;
self.correct_cursor_offset(store, frame, size, &old_blocks, &old_cursor_id, cursor) self.find_prev_msg(&mut tree, msg).await;
.await; }
_ => {}
} }
} }
pub async fn move_down<S: MsgStore<M>>( pub async fn move_down(&mut self, cursor: &mut Cursor<M::Id>) {
&mut self, if let Cursor::Msg(ref mut msg) = cursor {
store: &S, let path = self.store.path(msg).await;
cursor: &mut Option<Cursor<M::Id>>, let mut tree = self.store.tree(path.first()).await;
frame: &mut Frame, if !self.find_next_msg(&mut tree, msg).await {
size: Size, *cursor = Cursor::Bottom;
) {
let old_blocks = self
.layout_blocks(store, cursor.as_ref(), frame, size)
.await;
let old_cursor_id = cursor.as_ref().map(|c| c.id.clone());
if let Some(cursor) = cursor {
let path = store.path(&cursor.id).await;
let mut tree = store.tree(path.first()).await;
self.find_next_msg(store, &mut tree, &mut cursor.id).await;
} }
// If that condition doesn't hold, we're already at the bottom in
// cursor-less mode and can't move further down anyways.
if let Some(cursor) = cursor {
self.correct_cursor_offset(store, frame, size, &old_blocks, &old_cursor_id, cursor)
.await;
} }
} }
pub async fn move_to_first(&mut self, cursor: &mut Cursor<M::Id>) {
if let Some(tree_id) = self.store.first_tree().await {
*cursor = Cursor::Msg(tree_id);
}
}
pub async fn move_to_last(&mut self, cursor: &mut Cursor<M::Id>) {
*cursor = Cursor::Bottom;
}
}
/*
pub async fn move_up_sibling<S: MsgStore<M>>( pub async fn move_up_sibling<S: MsgStore<M>>(
&mut self, &mut self,
store: &S, store: &S,
@ -310,53 +254,6 @@ impl<M: Msg> TreeView<M> {
} }
} }
pub async fn move_to_first<S: MsgStore<M>>(
&mut self,
store: &S,
cursor: &mut Option<Cursor<M::Id>>,
frame: &mut Frame,
size: Size,
) {
let old_blocks = self
.layout_blocks(store, cursor.as_ref(), frame, size)
.await;
let old_cursor_id = cursor.as_ref().map(|c| c.id.clone());
if let Some(tree_id) = store.first_tree().await {
*cursor = Some(Cursor::new(tree_id));
}
if let Some(cursor) = cursor {
self.correct_cursor_offset(store, frame, size, &old_blocks, &old_cursor_id, cursor)
.await;
}
}
pub async fn move_to_last<S: MsgStore<M>>(
&mut self,
store: &S,
cursor: &mut Option<Cursor<M::Id>>,
frame: &mut Frame,
size: Size,
) {
let old_blocks = self
.layout_blocks(store, cursor.as_ref(), frame, size)
.await;
let old_cursor_id = cursor.as_ref().map(|c| c.id.clone());
if let Some(tree_id) = store.last_tree().await {
let tree = store.tree(&tree_id).await;
let mut id = tree_id;
while Self::find_last_child(&tree, &mut id) {}
*cursor = Some(Cursor::new(id));
}
if let Some(cursor) = cursor {
self.correct_cursor_offset(store, frame, size, &old_blocks, &old_cursor_id, cursor)
.await;
}
}
// TODO move_older[_unseen] // TODO move_older[_unseen]
// TODO move_newer[_unseen] // TODO move_newer[_unseen]
@ -378,4 +275,4 @@ impl<M: Msg> TreeView<M> {
.await; .await;
} }
} }
} */

View file

@ -5,7 +5,8 @@ use toss::frame::{Frame, Size};
use crate::store::{Msg, MsgStore, Path, Tree}; use crate::store::{Msg, MsgStore, Path, Tree};
use super::blocks::{Block, BlockBody, Blocks, MsgBlock}; use super::blocks::{Block, BlockBody, Blocks, MsgBlock};
use super::{util, Cursor, InnerTreeViewState}; use super::cursor::Cursor;
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> {