From 0ad3432141b3888960f8f087db3d540c6c66555e Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 9 Aug 2022 14:59:41 +0200 Subject: [PATCH] Fold subtrees --- src/store.rs | 4 ++++ src/ui/chat/tree.rs | 16 ++++++++++++++-- src/ui/chat/tree/layout.rs | 22 ++++++++++++++++++---- src/ui/chat/tree/widgets.rs | 37 +++++++++++++++++++++++++++++++++---- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/store.rs b/src/store.rs index aba9279..89bb530 100644 --- a/src/store.rs +++ b/src/store.rs @@ -23,6 +23,10 @@ impl Path { Self(segments) } + pub fn parent_segments(&self) -> impl Iterator { + self.0.iter().take(self.0.len() - 1) + } + pub fn push(&mut self, segment: I) { self.0.push(segment) } diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index ee87a86..8ab9f2a 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -3,6 +3,7 @@ mod layout; mod tree_blocks; mod widgets; +use std::collections::HashSet; use std::sync::Arc; use async_trait::async_trait; @@ -39,13 +40,14 @@ struct InnerTreeViewState> { last_visible_msgs: Vec, cursor: Cursor, + editor: EditorState, /// Scroll the view on the next render. Positive values scroll up and /// negative values scroll down. scroll: i32, correction: Option, - editor: EditorState, + folded: HashSet, } impl> InnerTreeViewState { @@ -56,9 +58,10 @@ impl> InnerTreeViewState { last_cursor_line: 0, last_visible_msgs: vec![], cursor: Cursor::Bottom, + editor: EditorState::new(), scroll: 0, correction: None, - editor: EditorState::new(), + folded: HashSet::new(), } } @@ -98,6 +101,7 @@ impl> InnerTreeViewState { } pub fn list_action_key_bindings(&self, bindings: &mut KeyBindingsList) { + bindings.binding("space", "fold current message's subtree"); bindings.binding("s", "toggle current message's seen status"); bindings.binding("S", "mark all visible messages as seen"); bindings.binding("ctrl+s", "mark all older messages as seen"); @@ -105,6 +109,14 @@ impl> InnerTreeViewState { async fn handle_action_key_event(&mut self, event: KeyEvent, id: Option<&M::Id>) -> bool { match event { + key!(' ') => { + if let Some(id) = id { + if !self.folded.remove(id) { + self.folded.insert(id.clone()); + } + return true; + } + } key!('s') => { if let Some(id) = id { if let Some(msg) = self.store.tree(id).await.msg(id) { diff --git a/src/ui/chat/tree/layout.rs b/src/ui/chat/tree/layout.rs index 6fb4911..32f8ecf 100644 --- a/src/ui/chat/tree/layout.rs +++ b/src/ui/chat/tree/layout.rs @@ -38,6 +38,12 @@ impl> InnerTreeViewState { } } + fn make_path_visible(&mut self, path: &Path) { + for segment in path.parent_segments() { + self.folded.remove(segment); + } + } + fn cursor_line(&self, blocks: &TreeBlocks) -> i32 { if let Cursor::Bottom = self.cursor { // The value doesn't matter as it will always be ignored. @@ -84,18 +90,25 @@ impl> InnerTreeViewState { blocks.blocks_mut().push_back(block); } + let folded = self.folded.contains(id); + let children = tree.children(id); + let folded_info = children + .filter(|_| folded) + .map(|c| c.len()) + .filter(|c| *c > 0); + // Main message body let highlighted = self.cursor.refers_to(id); let widget = if let Some(msg) = tree.msg(id) { - widgets::msg(highlighted, indent, msg) + widgets::msg(highlighted, indent, msg, folded_info) } else { - widgets::msg_placeholder(highlighted, indent) + widgets::msg_placeholder(highlighted, indent, folded_info) }; 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) { + // Children recursively (if not folded) + if let Some(children) = children.filter(|_| !folded) { for child in children { self.layout_subtree(nick, frame, tree, indent + 1, child, blocks); } @@ -458,6 +471,7 @@ impl> InnerTreeViewState { let last_cursor_path = self.cursor_path(&self.last_cursor).await; let cursor_path = self.cursor_path(&self.cursor).await; + self.make_path_visible(&cursor_path); let mut blocks = self .layout_initial_seed(nick, frame, &last_cursor_path, &cursor_path) diff --git a/src/ui/chat/tree/widgets.rs b/src/ui/chat/tree/widgets.rs index b39bd5c..2ba3d14 100644 --- a/src/ui/chat/tree/widgets.rs +++ b/src/ui/chat/tree/widgets.rs @@ -6,6 +6,7 @@ mod time; use crossterm::style::{ContentStyle, Stylize}; use toss::frame::Frame; +use toss::styled::Styled; use super::super::ChatMsg; use crate::store::Msg; @@ -40,6 +41,10 @@ fn style_indent(highlighted: bool) -> ContentStyle { } } +fn style_info() -> ContentStyle { + ContentStyle::default().italic().dark_grey() +} + fn style_editor_highlight() -> ContentStyle { ContentStyle::default().black().on_cyan() } @@ -48,8 +53,20 @@ fn style_pseudo_highlight() -> ContentStyle { ContentStyle::default().black().on_yellow() } -pub fn msg(highlighted: bool, indent: usize, msg: &M) -> BoxedWidget { - let (nick, content) = msg.styled(); +pub fn msg( + highlighted: bool, + indent: usize, + msg: &M, + folded_info: Option, +) -> BoxedWidget { + let (nick, mut content) = msg.styled(); + + if let Some(amount) = folded_info { + content = content + .then_plain("\n") + .then(format!("[{amount} more]"), style_info()); + } + HJoin::new(vec![ Segment::new(seen::widget(msg.seen())), Segment::new( @@ -69,7 +86,19 @@ pub fn msg(highlighted: bool, indent: usize, msg: &M) -> Boxed .into() } -pub fn msg_placeholder(highlighted: bool, indent: usize) -> BoxedWidget { +pub fn msg_placeholder( + highlighted: bool, + indent: usize, + folded_info: Option, +) -> BoxedWidget { + let mut content = Styled::new(PLACEHOLDER, style_placeholder()); + + if let Some(amount) = folded_info { + content = content + .then_plain("\n") + .then(format!("[{amount} more]"), style_info()); + } + HJoin::new(vec![ Segment::new(seen::widget(true)), Segment::new( @@ -78,7 +107,7 @@ pub fn msg_placeholder(highlighted: bool, indent: usize) -> BoxedWidget { .right(1), ), Segment::new(Indent::new(indent, style_indent(highlighted))), - Segment::new(Text::new((PLACEHOLDER, style_placeholder()))), + Segment::new(Text::new(content)), ]) .into() }