From bc8c5968d6bebe1f0fd00599aa01cfa9404ffe49 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 17 Apr 2023 10:08:14 +0200 Subject: [PATCH] Remove old chat, widgets, util modules --- src/ui.rs | 3 - src/ui/chat.rs | 163 -------- src/ui/chat/blocks.rs | 171 -------- src/ui/chat/tree.rs | 458 --------------------- src/ui/chat/tree/cursor.rs | 498 ----------------------- src/ui/chat/tree/layout.rs | 612 ----------------------------- src/ui/chat/tree/tree_blocks.rs | 71 ---- src/ui/chat/tree/widgets.rs | 165 -------- src/ui/chat/tree/widgets/indent.rs | 41 -- src/ui/chat/tree/widgets/seen.rs | 25 -- src/ui/chat/tree/widgets/time.rs | 25 -- src/ui/util.rs | 166 -------- src/ui/widgets.rs | 109 ----- src/ui/widgets/background.rs | 46 --- src/ui/widgets/border.rs | 65 --- src/ui/widgets/cursor.rs | 44 --- src/ui/widgets/editor.rs | 566 -------------------------- src/ui/widgets/empty.rs | 44 --- src/ui/widgets/float.rs | 73 ---- src/ui/widgets/join.rs | 265 ------------- src/ui/widgets/layer.rs | 38 -- src/ui/widgets/list.rs | 395 ------------------- src/ui/widgets/padding.rs | 103 ----- src/ui/widgets/popup.rs | 74 ---- src/ui/widgets/resize.rs | 86 ---- src/ui/widgets/rules.rs | 46 --- src/ui/widgets/text.rs | 65 --- 27 files changed, 4417 deletions(-) delete mode 100644 src/ui/chat.rs delete mode 100644 src/ui/chat/blocks.rs delete mode 100644 src/ui/chat/tree.rs delete mode 100644 src/ui/chat/tree/cursor.rs delete mode 100644 src/ui/chat/tree/layout.rs delete mode 100644 src/ui/chat/tree/tree_blocks.rs delete mode 100644 src/ui/chat/tree/widgets.rs delete mode 100644 src/ui/chat/tree/widgets/indent.rs delete mode 100644 src/ui/chat/tree/widgets/seen.rs delete mode 100644 src/ui/chat/tree/widgets/time.rs delete mode 100644 src/ui/util.rs delete mode 100644 src/ui/widgets.rs delete mode 100644 src/ui/widgets/background.rs delete mode 100644 src/ui/widgets/border.rs delete mode 100644 src/ui/widgets/cursor.rs delete mode 100644 src/ui/widgets/editor.rs delete mode 100644 src/ui/widgets/empty.rs delete mode 100644 src/ui/widgets/float.rs delete mode 100644 src/ui/widgets/join.rs delete mode 100644 src/ui/widgets/layer.rs delete mode 100644 src/ui/widgets/list.rs delete mode 100644 src/ui/widgets/padding.rs delete mode 100644 src/ui/widgets/popup.rs delete mode 100644 src/ui/widgets/resize.rs delete mode 100644 src/ui/widgets/rules.rs delete mode 100644 src/ui/widgets/text.rs diff --git a/src/ui.rs b/src/ui.rs index 654c819..df3371c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,11 +1,8 @@ -mod chat; mod chat2; mod euph; mod input; mod rooms; -mod util; mod util2; -mod widgets; mod widgets2; use std::convert::Infallible; diff --git a/src/ui/chat.rs b/src/ui/chat.rs deleted file mode 100644 index f9f0367..0000000 --- a/src/ui/chat.rs +++ /dev/null @@ -1,163 +0,0 @@ -// TODO Implement thread view -// TODO Implement flat (chronological?) view -// TODO Implement message search? - -mod blocks; -mod tree; - -use std::sync::Arc; -use std::{fmt, io}; - -use async_trait::async_trait; -use parking_lot::FairMutex; -use time::OffsetDateTime; -use toss::{Frame, Size, Styled, Terminal, WidthDb}; - -use crate::store::{Msg, MsgStore}; - -use self::tree::{TreeView, TreeViewState}; - -use super::input::{InputEvent, KeyBindingsList}; -use super::widgets::Widget; - -/////////// -// Trait // -/////////// - -pub trait ChatMsg { - fn time(&self) -> OffsetDateTime; - fn styled(&self) -> (Styled, Styled); - fn edit(nick: &str, content: &str) -> (Styled, Styled); - fn pseudo(nick: &str, content: &str) -> (Styled, Styled); -} - -/////////// -// State // -/////////// - -pub enum Mode { - Tree, - // Thread, - // Flat, -} - -pub struct ChatState> { - store: S, - mode: Mode, - tree: TreeViewState, - // thread: ThreadView, - // flat: FlatView, -} - -impl + Clone> ChatState { - pub fn new(store: S) -> Self { - Self { - mode: Mode::Tree, - tree: TreeViewState::new(store.clone()), - store, - } - } -} - -impl> ChatState { - pub fn store(&self) -> &S { - &self.store - } - - pub fn widget(&self, nick: String, focused: bool) -> Chat { - match self.mode { - Mode::Tree => Chat::Tree(self.tree.widget(nick, focused)), - } - } -} - -pub enum Reaction { - NotHandled, - Handled, - Composed { - parent: Option, - content: String, - }, - ComposeError(io::Error), -} - -impl Reaction { - pub fn handled(&self) -> bool { - !matches!(self, Self::NotHandled) - } -} - -impl> ChatState { - pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) { - match self.mode { - Mode::Tree => self.tree.list_key_bindings(bindings, can_compose).await, - } - } - - pub async fn handle_input_event( - &mut self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - event: &InputEvent, - can_compose: bool, - ) -> Result, S::Error> { - match self.mode { - Mode::Tree => { - self.tree - .handle_input_event(terminal, crossterm_lock, event, can_compose) - .await - } - } - } - - pub async fn cursor(&self) -> Option { - match self.mode { - Mode::Tree => self.tree.cursor().await, - } - } - - /// A [`Reaction::Composed`] message was sent, either successfully or - /// unsuccessfully. - /// - /// If successful, include the message's id as an argument. If unsuccessful, - /// instead pass a `None`. - pub async fn sent(&mut self, id: Option) { - match self.mode { - Mode::Tree => self.tree.sent(id).await, - } - } -} - -//////////// -// Widget // -//////////// - -pub enum Chat> { - Tree(TreeView), -} - -#[async_trait] -impl Widget for Chat -where - M: Msg + ChatMsg + Send + Sync, - M::Id: Send + Sync, - S: MsgStore + Send + Sync, - S::Error: fmt::Display, -{ - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - match self { - Self::Tree(tree) => tree.size(widthdb, max_width, max_height).await, - } - } - - async fn render(self: Box, frame: &mut Frame) { - match *self { - Self::Tree(tree) => Box::new(tree).render(frame).await, - } - } -} diff --git a/src/ui/chat/blocks.rs b/src/ui/chat/blocks.rs deleted file mode 100644 index bb596cb..0000000 --- a/src/ui/chat/blocks.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::collections::{vec_deque, VecDeque}; -use std::ops::Range; - -use toss::Frame; - -use crate::macros::some_or_return; -use crate::ui::widgets::BoxedWidget; - -pub struct Block { - pub id: I, - pub top_line: i32, - pub height: i32, - /// The lines of the block that should be made visible if the block is - /// 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 - /// should be preferred over the bottom. - pub focus: Range, - pub widget: BoxedWidget, -} - -impl Block { - pub async fn new>(frame: &mut Frame, id: I, widget: W) -> Self { - // Interestingly, rust-analyzer fails to deduce the type of `widget` - // here but rustc knows it's a `BoxedWidget`. - let widget = widget.into(); - let max_width = frame.size().width; - let size = widget.size(frame.widthdb(), Some(max_width), None).await; - let height = size.height.into(); - Self { - id, - top_line: 0, - height, - focus: 0..height, - widget, - } - } - - pub fn focus(mut self, focus: Range) -> Self { - self.focus = focus; - self - } -} - -pub struct Blocks { - pub blocks: VecDeque>, - /// The top line of the first block. Useful for prepending blocks, - /// especially to empty [`Blocks`]s. - pub top_line: i32, - /// The bottom line of the last block. Useful for appending blocks, - /// especially to empty [`Blocks`]s. - pub bottom_line: i32, -} - -impl Blocks { - pub fn new() -> Self { - Self::new_below(0) - } - - /// Create a new [`Blocks`] such that the first prepended line will be on - /// `line`. - pub fn new_below(line: i32) -> Self { - Self { - blocks: VecDeque::new(), - top_line: line + 1, - bottom_line: line, - } - } - - pub fn iter(&self) -> vec_deque::Iter<'_, Block> { - self.blocks.iter() - } - - pub fn offset(&mut self, delta: i32) { - self.top_line += delta; - self.bottom_line += delta; - for block in &mut self.blocks { - block.top_line += delta; - } - } - - pub fn push_front(&mut self, mut block: Block) { - self.top_line -= block.height; - block.top_line = self.top_line; - self.blocks.push_front(block); - } - - pub fn push_back(&mut self, mut block: Block) { - block.top_line = self.bottom_line + 1; - self.bottom_line += block.height; - self.blocks.push_back(block); - } - - pub fn prepend(&mut self, mut layout: Self) { - while let Some(block) = layout.blocks.pop_back() { - self.push_front(block); - } - } - - pub fn append(&mut self, mut layout: Self) { - while let Some(block) = layout.blocks.pop_front() { - 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 Blocks { - pub fn find(&self, id: &I) -> Option<&Block> { - 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 - .blocks - .iter() - .enumerate() - .find(|(_, b)| b.id == *id) - .map(|(i, _)| i)); - - self.blocks[idx].top_line = top_line; - - // Propagate changes to top - for i in (0..idx).rev() { - self.blocks[i].top_line = self.blocks[i + 1].top_line - self.blocks[i].height; - } - self.top_line = self.blocks.front().expect("blocks nonempty").top_line; - - // Propagate changes to bottom - for i in (idx + 1)..self.blocks.len() { - self.blocks[i].top_line = self.blocks[i - 1].top_line + self.blocks[i - 1].height; - } - let bottom = self.blocks.back().expect("blocks nonempty"); - self.bottom_line = bottom.top_line + bottom.height - 1; - } -} diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs deleted file mode 100644 index 49e1392..0000000 --- a/src/ui/chat/tree.rs +++ /dev/null @@ -1,458 +0,0 @@ -// TODO Focusing on sub-trees - -mod cursor; -mod layout; -mod tree_blocks; -mod widgets; - -use std::collections::HashSet; -use std::fmt; -use std::sync::Arc; - -use async_trait::async_trait; -use parking_lot::FairMutex; -use tokio::sync::Mutex; -use toss::{Frame, Pos, Size, Terminal, WidthDb}; - -use crate::macros::logging_unwrap; -use crate::store::{Msg, MsgStore}; -use crate::ui::input::{key, InputEvent, KeyBindingsList}; -use crate::ui::util; -use crate::ui::widgets::editor::EditorState; -use crate::ui::widgets::Widget; - -use self::cursor::Cursor; - -use super::{ChatMsg, Reaction}; - -/////////// -// State // -/////////// - -enum Correction { - MakeCursorVisible, - MoveCursorToVisibleArea, - CenterCursor, -} - -struct InnerTreeViewState> { - store: S, - - last_cursor: Cursor, - last_cursor_line: i32, - 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, - - folded: HashSet, -} - -impl> InnerTreeViewState { - fn new(store: S) -> Self { - Self { - store, - last_cursor: Cursor::Bottom, - last_cursor_line: 0, - last_visible_msgs: vec![], - cursor: Cursor::Bottom, - editor: EditorState::new(), - scroll: 0, - correction: None, - folded: HashSet::new(), - } - } - - pub fn list_movement_key_bindings(&self, bindings: &mut KeyBindingsList) { - bindings.binding("j/k, ↓/↑", "move cursor up/down"); - bindings.binding("J/K, ctrl+↓/↑", "move cursor to prev/next sibling"); - bindings.binding("p/P", "move cursor to parent/root"); - bindings.binding("h/l, ←/→", "move cursor chronologically"); - bindings.binding("H/L, ctrl+←/→", "move cursor to prev/next unseen message"); - bindings.binding("g, home", "move cursor to top"); - bindings.binding("G, end", "move cursor to bottom"); - bindings.binding("ctrl+y/e", "scroll up/down a line"); - bindings.binding("ctrl+u/d", "scroll up/down half a screen"); - bindings.binding("ctrl+b/f, page up/down", "scroll up/down one screen"); - bindings.binding("z", "center cursor on screen"); - // TODO Bindings inspired by vim's ()/[]/{} bindings? - } - - async fn handle_movement_input_event( - &mut self, - frame: &mut Frame, - event: &InputEvent, - ) -> Result { - let chat_height = frame.size().height - 3; - - match event { - key!('k') | key!(Up) => self.move_cursor_up().await?, - key!('j') | key!(Down) => self.move_cursor_down().await?, - key!('K') | key!(Ctrl + Up) => self.move_cursor_up_sibling().await?, - key!('J') | key!(Ctrl + Down) => self.move_cursor_down_sibling().await?, - key!('p') => self.move_cursor_to_parent().await?, - key!('P') => self.move_cursor_to_root().await?, - key!('h') | key!(Left) => self.move_cursor_older().await?, - key!('l') | key!(Right) => self.move_cursor_newer().await?, - key!('H') | key!(Ctrl + Left) => self.move_cursor_older_unseen().await?, - key!('L') | key!(Ctrl + Right) => self.move_cursor_newer_unseen().await?, - key!('g') | key!(Home) => self.move_cursor_to_top().await?, - key!('G') | key!(End) => self.move_cursor_to_bottom().await, - key!(Ctrl + 'y') => self.scroll_up(1), - key!(Ctrl + 'e') => self.scroll_down(1), - key!(Ctrl + 'u') => self.scroll_up((chat_height / 2).into()), - key!(Ctrl + 'd') => self.scroll_down((chat_height / 2).into()), - key!(Ctrl + 'b') | key!(PageUp) => self.scroll_up(chat_height.saturating_sub(1).into()), - key!(Ctrl + 'f') | key!(PageDown) => { - self.scroll_down(chat_height.saturating_sub(1).into()) - } - key!('z') => self.center_cursor(), - _ => return Ok(false), - } - - Ok(true) - } - - 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"); - } - - async fn handle_action_input_event( - &mut self, - event: &InputEvent, - id: Option<&M::Id>, - ) -> Result { - match event { - key!(' ') => { - if let Some(id) = id { - if !self.folded.remove(id) { - self.folded.insert(id.clone()); - } - return Ok(true); - } - } - key!('s') => { - if let Some(id) = id { - if let Some(msg) = self.store.tree(id).await?.msg(id) { - self.store.set_seen(id, !msg.seen()).await?; - } - return Ok(true); - } - } - key!('S') => { - for id in &self.last_visible_msgs { - self.store.set_seen(id, true).await?; - } - return Ok(true); - } - key!(Ctrl + 's') => { - if let Some(id) = id { - self.store.set_older_seen(id, true).await?; - } else { - self.store - .set_older_seen(&M::last_possible_id(), true) - .await?; - } - return Ok(true); - } - _ => {} - } - Ok(false) - } - - pub fn list_edit_initiating_key_bindings(&self, bindings: &mut KeyBindingsList) { - bindings.binding("r", "reply to message (inline if possible, else directly)"); - bindings.binding("R", "reply to message (opposite of R)"); - bindings.binding("t", "start a new thread"); - } - - async fn handle_edit_initiating_input_event( - &mut self, - event: &InputEvent, - id: Option, - ) -> Result { - match event { - key!('r') => { - if let Some(parent) = self.parent_for_normal_reply().await? { - self.cursor = Cursor::editor(id, parent); - self.correction = Some(Correction::MakeCursorVisible); - } - } - key!('R') => { - if let Some(parent) = self.parent_for_alternate_reply().await? { - self.cursor = Cursor::editor(id, parent); - self.correction = Some(Correction::MakeCursorVisible); - } - } - key!('t') | key!('T') => { - self.cursor = Cursor::editor(id, None); - self.correction = Some(Correction::MakeCursorVisible); - } - _ => return Ok(false), - } - - Ok(true) - } - - pub fn list_normal_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) { - self.list_movement_key_bindings(bindings); - bindings.empty(); - self.list_action_key_bindings(bindings); - if can_compose { - bindings.empty(); - self.list_edit_initiating_key_bindings(bindings); - } - } - - async fn handle_normal_input_event( - &mut self, - frame: &mut Frame, - event: &InputEvent, - can_compose: bool, - id: Option, - ) -> Result { - #[allow(clippy::if_same_then_else)] - Ok(if self.handle_movement_input_event(frame, event).await? { - true - } else if self.handle_action_input_event(event, id.as_ref()).await? { - true - } else if can_compose { - self.handle_edit_initiating_input_event(event, id).await? - } else { - false - }) - } - - fn list_editor_key_bindings(&self, bindings: &mut KeyBindingsList) { - bindings.binding("esc", "close editor"); - bindings.binding("enter", "send message"); - util::list_editor_key_bindings_allowing_external_editing(bindings, |_| true); - } - - fn handle_editor_input_event( - &mut self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - event: &InputEvent, - coming_from: Option, - parent: Option, - ) -> Reaction { - // TODO Tab-completion - match event { - key!(Esc) => { - self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom); - self.correction = Some(Correction::MakeCursorVisible); - return Reaction::Handled; - } - - key!(Enter) => { - let content = self.editor.text(); - if !content.trim().is_empty() { - self.cursor = Cursor::Pseudo { - coming_from, - parent: parent.clone(), - }; - return Reaction::Composed { parent, content }; - } - } - - _ => { - let handled = util::handle_editor_input_event_allowing_external_editing( - &self.editor, - terminal, - crossterm_lock, - event, - |_| true, - ); - match handled { - Ok(true) => {} - Ok(false) => return Reaction::NotHandled, - Err(e) => return Reaction::ComposeError(e), - } - } - } - - self.correction = Some(Correction::MakeCursorVisible); - Reaction::Handled - } - - pub fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) { - bindings.heading("Chat"); - match &self.cursor { - Cursor::Bottom | Cursor::Msg(_) => { - self.list_normal_key_bindings(bindings, can_compose); - } - Cursor::Editor { .. } => self.list_editor_key_bindings(bindings), - Cursor::Pseudo { .. } => { - self.list_normal_key_bindings(bindings, false); - } - } - } - - async fn handle_input_event( - &mut self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - event: &InputEvent, - can_compose: bool, - ) -> Result, S::Error> { - Ok(match &self.cursor { - Cursor::Bottom => { - if self - .handle_normal_input_event(terminal.frame(), event, can_compose, None) - .await? - { - Reaction::Handled - } else { - Reaction::NotHandled - } - } - Cursor::Msg(id) => { - let id = id.clone(); - if self - .handle_normal_input_event(terminal.frame(), event, can_compose, Some(id)) - .await? - { - Reaction::Handled - } else { - Reaction::NotHandled - } - } - Cursor::Editor { - coming_from, - parent, - } => self.handle_editor_input_event( - terminal, - crossterm_lock, - event, - coming_from.clone(), - parent.clone(), - ), - Cursor::Pseudo { .. } => { - if self - .handle_movement_input_event(terminal.frame(), event) - .await? - { - Reaction::Handled - } else { - Reaction::NotHandled - } - } - }) - } - - fn cursor(&self) -> Option { - match &self.cursor { - Cursor::Msg(id) => Some(id.clone()), - Cursor::Bottom | Cursor::Editor { .. } | Cursor::Pseudo { .. } => None, - } - } - - fn sent(&mut self, id: Option) { - if let Cursor::Pseudo { coming_from, .. } = &self.cursor { - if let Some(id) = id { - self.last_cursor = Cursor::Msg(id.clone()); - self.cursor = Cursor::Msg(id); - self.editor.clear(); - } else { - self.cursor = match coming_from { - Some(id) => Cursor::Msg(id.clone()), - None => Cursor::Bottom, - }; - }; - } - } -} - -pub struct TreeViewState>(Arc>>); - -impl> TreeViewState { - pub fn new(store: S) -> Self { - Self(Arc::new(Mutex::new(InnerTreeViewState::new(store)))) - } - - pub fn widget(&self, nick: String, focused: bool) -> TreeView { - TreeView { - inner: self.0.clone(), - nick, - focused, - } - } - - pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) { - self.0.lock().await.list_key_bindings(bindings, can_compose); - } - - pub async fn handle_input_event( - &mut self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - event: &InputEvent, - can_compose: bool, - ) -> Result, S::Error> { - self.0 - .lock() - .await - .handle_input_event(terminal, crossterm_lock, event, can_compose) - .await - } - - pub async fn cursor(&self) -> Option { - self.0.lock().await.cursor() - } - - pub async fn sent(&mut self, id: Option) { - self.0.lock().await.sent(id) - } -} - -//////////// -// Widget // -//////////// - -pub struct TreeView> { - inner: Arc>>, - nick: String, - focused: bool, -} - -#[async_trait] -impl Widget for TreeView -where - M: Msg + ChatMsg + Send + Sync, - M::Id: Send + Sync, - S: MsgStore + Send + Sync, - S::Error: fmt::Display, -{ - async fn size( - &self, - _widthdb: &mut WidthDb, - _max_width: Option, - _max_height: Option, - ) -> Size { - Size::ZERO - } - - async fn render(self: Box, frame: &mut Frame) { - let mut guard = self.inner.lock().await; - let blocks = logging_unwrap!(guard.relayout(self.nick, self.focused, 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(); - } - } -} diff --git a/src/ui/chat/tree/cursor.rs b/src/ui/chat/tree/cursor.rs deleted file mode 100644 index e154708..0000000 --- a/src/ui/chat/tree/cursor.rs +++ /dev/null @@ -1,498 +0,0 @@ -//! Moving the cursor around. - -use std::collections::HashSet; - -use crate::store::{Msg, MsgStore, Tree}; - -use super::{Correction, InnerTreeViewState}; - -#[derive(Debug, Clone, Copy)] -pub enum Cursor { - Bottom, - Msg(I), - Editor { - coming_from: Option, - parent: Option, - }, - Pseudo { - coming_from: Option, - parent: Option, - }, -} - -impl Cursor { - pub fn editor(coming_from: Option, parent: Option) -> Self { - Self::Editor { - coming_from, - parent, - } - } -} - -impl Cursor { - 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 { - parent: Some(parent), - .. - } - | Self::Pseudo { - parent: Some(parent), - .. - } = self - { - parent == id - } else { - false - } - } -} - -impl> InnerTreeViewState { - fn find_parent(tree: &Tree, id: &mut M::Id) -> bool { - if let Some(parent) = tree.parent(id) { - *id = parent; - true - } else { - false - } - } - - fn find_first_child(folded: &HashSet, tree: &Tree, id: &mut M::Id) -> bool { - if folded.contains(id) { - return false; - } - - if let Some(child) = tree.children(id).and_then(|c| c.first()) { - *id = child.clone(); - true - } else { - false - } - } - - fn find_last_child(folded: &HashSet, tree: &Tree, id: &mut M::Id) -> bool { - if folded.contains(id) { - return false; - } - - if let Some(child) = tree.children(id).and_then(|c| c.last()) { - *id = child.clone(); - true - } else { - false - } - } - - /// Move to the previous sibling, or don't move if this is not possible. - /// - /// Always stays at the same level of indentation. - async fn find_prev_sibling( - store: &S, - tree: &mut Tree, - id: &mut M::Id, - ) -> Result { - let moved = if let Some(prev_sibling) = tree.prev_sibling(id) { - *id = prev_sibling; - true - } else if tree.parent(id).is_none() { - // We're at the root of our tree, so we need to move to the root of - // the previous tree. - if let Some(prev_root_id) = store.prev_root_id(tree.root()).await? { - *tree = store.tree(&prev_root_id).await?; - *id = prev_root_id; - true - } else { - false - } - } else { - false - }; - Ok(moved) - } - - /// Move to the next sibling, or don't move if this is not possible. - /// - /// Always stays at the same level of indentation. - async fn find_next_sibling( - store: &S, - tree: &mut Tree, - id: &mut M::Id, - ) -> Result { - let moved = if let Some(next_sibling) = tree.next_sibling(id) { - *id = next_sibling; - true - } else if tree.parent(id).is_none() { - // We're at the root of our tree, so we need to move to the root of - // the next tree. - if let Some(next_root_id) = store.next_root_id(tree.root()).await? { - *tree = store.tree(&next_root_id).await?; - *id = next_root_id; - true - } else { - false - } - } else { - false - }; - Ok(moved) - } - - /// Move to the previous message, or don't move if this is not possible. - async fn find_prev_msg( - store: &S, - folded: &HashSet, - tree: &mut Tree, - id: &mut M::Id, - ) -> Result { - // Move to previous sibling, then to its last child - // If not possible, move to parent - let moved = if Self::find_prev_sibling(store, tree, id).await? { - while Self::find_last_child(folded, tree, id) {} - true - } else { - Self::find_parent(tree, id) - }; - Ok(moved) - } - - /// Move to the next message, or don't move if this is not possible. - async fn find_next_msg( - store: &S, - folded: &HashSet, - tree: &mut Tree, - id: &mut M::Id, - ) -> Result { - if Self::find_first_child(folded, tree, id) { - return Ok(true); - } - - if Self::find_next_sibling(store, tree, id).await? { - return Ok(true); - } - - // Temporary id to avoid modifying the original one if no parent-sibling - // can be found. - let mut tmp_id = id.clone(); - while Self::find_parent(tree, &mut tmp_id) { - if Self::find_next_sibling(store, tree, &mut tmp_id).await? { - *id = tmp_id; - return Ok(true); - } - } - - Ok(false) - } - - pub async fn move_cursor_up(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Bottom | Cursor::Pseudo { parent: None, .. } => { - if let Some(last_root_id) = self.store.last_root_id().await? { - let tree = self.store.tree(&last_root_id).await?; - let mut id = last_root_id; - while Self::find_last_child(&self.folded, &tree, &mut id) {} - self.cursor = Cursor::Msg(id); - } - } - Cursor::Msg(msg) => { - let path = self.store.path(msg).await?; - let mut tree = self.store.tree(path.first()).await?; - Self::find_prev_msg(&self.store, &self.folded, &mut tree, msg).await?; - } - Cursor::Editor { .. } => {} - Cursor::Pseudo { - parent: Some(parent), - .. - } => { - let tree = self.store.tree(parent).await?; - let mut id = parent.clone(); - while Self::find_last_child(&self.folded, &tree, &mut id) {} - self.cursor = Cursor::Msg(id); - } - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_down(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Msg(msg) => { - let path = self.store.path(msg).await?; - let mut tree = self.store.tree(path.first()).await?; - if !Self::find_next_msg(&self.store, &self.folded, &mut tree, msg).await? { - self.cursor = Cursor::Bottom; - } - } - Cursor::Pseudo { parent: None, .. } => { - self.cursor = Cursor::Bottom; - } - Cursor::Pseudo { - parent: Some(parent), - .. - } => { - let mut tree = self.store.tree(parent).await?; - let mut id = parent.clone(); - while Self::find_last_child(&self.folded, &tree, &mut id) {} - // Now we're at the previous message - if Self::find_next_msg(&self.store, &self.folded, &mut tree, &mut id).await? { - self.cursor = Cursor::Msg(id); - } else { - self.cursor = Cursor::Bottom; - } - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_up_sibling(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Bottom | Cursor::Pseudo { parent: None, .. } => { - if let Some(last_root_id) = self.store.last_root_id().await? { - self.cursor = Cursor::Msg(last_root_id); - } - } - Cursor::Msg(msg) => { - let path = self.store.path(msg).await?; - let mut tree = self.store.tree(path.first()).await?; - Self::find_prev_sibling(&self.store, &mut tree, msg).await?; - } - Cursor::Editor { .. } => {} - Cursor::Pseudo { - parent: Some(parent), - .. - } => { - let path = self.store.path(parent).await?; - let tree = self.store.tree(path.first()).await?; - if let Some(children) = tree.children(parent) { - if let Some(last_child) = children.last() { - self.cursor = Cursor::Msg(last_child.clone()); - } - } - } - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_down_sibling(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Msg(msg) => { - let path = self.store.path(msg).await?; - let mut tree = self.store.tree(path.first()).await?; - if !Self::find_next_sibling(&self.store, &mut tree, msg).await? - && tree.parent(msg).is_none() - { - self.cursor = Cursor::Bottom; - } - } - Cursor::Pseudo { parent: None, .. } => { - self.cursor = Cursor::Bottom; - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_to_parent(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Pseudo { - parent: Some(parent), - .. - } => self.cursor = Cursor::Msg(parent.clone()), - Cursor::Msg(id) => { - // Could also be done via retrieving the path, but it doesn't - // really matter here - let tree = self.store.tree(id).await?; - Self::find_parent(&tree, id); - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_to_root(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Pseudo { - parent: Some(parent), - .. - } => { - let path = self.store.path(parent).await?; - self.cursor = Cursor::Msg(path.first().clone()); - } - Cursor::Msg(msg) => { - let path = self.store.path(msg).await?; - *msg = path.first().clone(); - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_older(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Msg(id) => { - if let Some(prev_id) = self.store.older_msg_id(id).await? { - *id = prev_id; - } - } - Cursor::Bottom | Cursor::Pseudo { .. } => { - if let Some(id) = self.store.newest_msg_id().await? { - self.cursor = Cursor::Msg(id); - } - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_newer(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Msg(id) => { - if let Some(prev_id) = self.store.newer_msg_id(id).await? { - *id = prev_id; - } else { - self.cursor = Cursor::Bottom; - } - } - Cursor::Pseudo { .. } => { - self.cursor = Cursor::Bottom; - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_older_unseen(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Msg(id) => { - if let Some(prev_id) = self.store.older_unseen_msg_id(id).await? { - *id = prev_id; - } - } - Cursor::Bottom | Cursor::Pseudo { .. } => { - if let Some(id) = self.store.newest_unseen_msg_id().await? { - self.cursor = Cursor::Msg(id); - } - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_newer_unseen(&mut self) -> Result<(), S::Error> { - match &mut self.cursor { - Cursor::Msg(id) => { - if let Some(prev_id) = self.store.newer_unseen_msg_id(id).await? { - *id = prev_id; - } else { - self.cursor = Cursor::Bottom; - } - } - Cursor::Pseudo { .. } => { - self.cursor = Cursor::Bottom; - } - _ => {} - } - self.correction = Some(Correction::MakeCursorVisible); - Ok(()) - } - - pub async fn move_cursor_to_top(&mut self) -> Result<(), S::Error> { - if let Some(first_root_id) = self.store.first_root_id().await? { - self.cursor = Cursor::Msg(first_root_id); - self.correction = Some(Correction::MakeCursorVisible); - } - Ok(()) - } - - pub async fn move_cursor_to_bottom(&mut self) { - self.cursor = Cursor::Bottom; - // Not really necessary; only here for consistency with other methods - self.correction = Some(Correction::MakeCursorVisible); - } - - pub fn scroll_up(&mut self, amount: i32) { - self.scroll += amount; - self.correction = Some(Correction::MoveCursorToVisibleArea); - } - - pub fn scroll_down(&mut self, amount: i32) { - self.scroll -= amount; - self.correction = Some(Correction::MoveCursorToVisibleArea); - } - - pub fn center_cursor(&mut self) { - self.correction = Some(Correction::CenterCursor); - } - - /// The outer `Option` shows whether a parent exists or not. The inner - /// `Option` shows if that parent has an id. - pub async fn parent_for_normal_reply(&self) -> Result>, S::Error> { - Ok(match &self.cursor { - Cursor::Bottom => Some(None), - Cursor::Msg(id) => { - let path = self.store.path(id).await?; - let tree = self.store.tree(path.first()).await?; - - Some(Some(if tree.next_sibling(id).is_some() { - // A reply to a message that has further siblings should be a - // direct reply. An indirect reply might end up a lot further - // down in the current conversation. - id.clone() - } else if let Some(parent) = tree.parent(id) { - // A reply to a message without younger siblings should be - // an indirect reply so as not to create unnecessarily deep - // threads. In the case that our message has children, this - // might get a bit confusing. I'm not sure yet how well this - // "smart" reply actually works in practice. - parent - } else { - // When replying to a top-level message, it makes sense to avoid - // creating unnecessary new threads. - id.clone() - })) - } - _ => None, - }) - } - - /// The outer `Option` shows whether a parent exists or not. The inner - /// `Option` shows if that parent has an id. - pub async fn parent_for_alternate_reply(&self) -> Result>, S::Error> { - Ok(match &self.cursor { - Cursor::Bottom => Some(None), - Cursor::Msg(id) => { - let path = self.store.path(id).await?; - let tree = self.store.tree(path.first()).await?; - - Some(Some(if tree.next_sibling(id).is_none() { - // The opposite of replying normally - id.clone() - } else if let Some(parent) = tree.parent(id) { - // The opposite of replying normally - parent - } else { - // The same as replying normally, still to avoid creating - // unnecessary new threads - id.clone() - })) - } - _ => None, - }) - } -} diff --git a/src/ui/chat/tree/layout.rs b/src/ui/chat/tree/layout.rs deleted file mode 100644 index df5fa4a..0000000 --- a/src/ui/chat/tree/layout.rs +++ /dev/null @@ -1,612 +0,0 @@ -use async_recursion::async_recursion; -use toss::Frame; - -use crate::store::{Msg, MsgStore, Path, Tree}; -use crate::ui::chat::blocks::Block; -use crate::ui::widgets::empty::Empty; -use crate::ui::ChatMsg; - -use super::tree_blocks::{BlockId, Root, TreeBlocks}; -use super::{widgets, Correction, Cursor, InnerTreeViewState}; - -const SCROLLOFF: i32 = 2; -const MIN_CONTENT_HEIGHT: i32 = 10; - -fn scrolloff(height: i32) -> i32 { - let scrolloff = (height - MIN_CONTENT_HEIGHT).max(0) / 2; - scrolloff.min(SCROLLOFF) -} - -struct Context { - nick: String, - focused: bool, -} - -impl InnerTreeViewState -where - M: Msg + ChatMsg + Send + Sync, - M::Id: Send + Sync, - S: MsgStore + Send + Sync, -{ - async fn cursor_path(&self, cursor: &Cursor) -> Result, S::Error> { - Ok(match cursor { - Cursor::Msg(id) => self.store.path(id).await?, - Cursor::Bottom - | Cursor::Editor { parent: None, .. } - | Cursor::Pseudo { parent: None, .. } => Path::new(vec![M::last_possible_id()]), - Cursor::Editor { - parent: Some(parent), - .. - } - | Cursor::Pseudo { - parent: Some(parent), - .. - } => { - let mut path = self.store.path(parent).await?; - path.push(M::last_possible_id()); - path - } - }) - } - - 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. - 0 - } else { - blocks - .blocks() - .find(&BlockId::from_cursor(&self.cursor)) - .expect("no cursor found") - .top_line - } - } - - fn contains_cursor(&self, blocks: &TreeBlocks) -> bool { - blocks - .blocks() - .find(&BlockId::from_cursor(&self.cursor)) - .is_some() - } - - async fn editor_block( - &self, - context: &Context, - frame: &mut Frame, - indent: usize, - ) -> Block> { - let (widget, cursor_row) = - widgets::editor::(frame.widthdb(), indent, &context.nick, &self.editor); - let cursor_row = cursor_row as i32; - Block::new(frame, BlockId::Cursor, widget) - .await - .focus(cursor_row..cursor_row + 1) - } - - async fn pseudo_block( - &self, - context: &Context, - frame: &mut Frame, - indent: usize, - ) -> Block> { - let widget = widgets::pseudo::(indent, &context.nick, &self.editor); - Block::new(frame, BlockId::Cursor, widget).await - } - - #[async_recursion] - async fn layout_subtree( - &self, - context: &Context, - frame: &mut Frame, - tree: &Tree, - indent: usize, - id: &M::Id, - blocks: &mut TreeBlocks, - ) { - // Ghost cursor in front, for positioning according to last cursor line - if self.last_cursor.refers_to(id) { - let block = Block::new(frame, BlockId::LastCursor, Empty::new()).await; - blocks.blocks_mut().push_back(block); - } - - // Last part of message body if message is folded - let folded = self.folded.contains(id); - let folded_info = if folded { - Some(tree.subtree_size(id)).filter(|s| *s > 0) - } else { - None - }; - - // Main message body - let highlighted = context.focused && self.cursor.refers_to(id); - let widget = if let Some(msg) = tree.msg(id) { - widgets::msg(highlighted, indent, msg, folded_info) - } else { - widgets::msg_placeholder(highlighted, indent, folded_info) - }; - let block = Block::new(frame, BlockId::Msg(id.clone()), widget).await; - blocks.blocks_mut().push_back(block); - - // Children, recursively - if !folded { - if let Some(children) = tree.children(id) { - for child in children { - self.layout_subtree(context, frame, tree, indent + 1, child, blocks) - .await; - } - } - } - - // 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::new()).await; - blocks.blocks_mut().push_back(block); - } - - // Trailing editor or pseudomessage - if self.cursor.refers_to_last_child_of(id) { - match self.cursor { - Cursor::Editor { .. } => blocks - .blocks_mut() - .push_back(self.editor_block(context, frame, indent + 1).await), - Cursor::Pseudo { .. } => blocks - .blocks_mut() - .push_back(self.pseudo_block(context, frame, indent + 1).await), - _ => {} - } - } - } - - async fn layout_tree( - &self, - context: &Context, - frame: &mut Frame, - tree: Tree, - ) -> TreeBlocks { - let root = Root::Tree(tree.root().clone()); - let mut blocks = TreeBlocks::new(root.clone(), root); - self.layout_subtree(context, frame, &tree, 0, tree.root(), &mut blocks) - .await; - blocks - } - - async fn layout_bottom(&self, context: &Context, frame: &mut Frame) -> TreeBlocks { - let mut blocks = TreeBlocks::new(Root::Bottom, Root::Bottom); - - // Ghost cursor, for positioning according to last cursor line - if let Cursor::Editor { parent: None, .. } | Cursor::Pseudo { parent: None, .. } = - self.last_cursor - { - let block = Block::new(frame, BlockId::LastCursor, Empty::new()).await; - blocks.blocks_mut().push_back(block); - } - - match self.cursor { - Cursor::Bottom => { - let block = Block::new(frame, BlockId::Cursor, Empty::new()).await; - blocks.blocks_mut().push_back(block); - } - Cursor::Editor { parent: None, .. } => blocks - .blocks_mut() - .push_back(self.editor_block(context, frame, 0).await), - Cursor::Pseudo { parent: None, .. } => blocks - .blocks_mut() - .push_back(self.pseudo_block(context, frame, 0).await), - _ => {} - } - - blocks - } - - async fn expand_to_top( - &self, - context: &Context, - frame: &mut Frame, - blocks: &mut TreeBlocks, - ) -> Result<(), S::Error> { - let top_line = 0; - - while blocks.blocks().top_line > top_line { - let top_root = blocks.top_root(); - let prev_root_id = match top_root { - Root::Bottom => self.store.last_root_id().await?, - Root::Tree(root_id) => self.store.prev_root_id(root_id).await?, - }; - let prev_root_id = match prev_root_id { - Some(id) => id, - None => break, - }; - let prev_tree = self.store.tree(&prev_root_id).await?; - blocks.prepend(self.layout_tree(context, frame, prev_tree).await); - } - - Ok(()) - } - - async fn expand_to_bottom( - &self, - context: &Context, - frame: &mut Frame, - blocks: &mut TreeBlocks, - ) -> Result<(), S::Error> { - let bottom_line = frame.size().height as i32 - 1; - - while blocks.blocks().bottom_line < bottom_line { - let bottom_root = blocks.bottom_root(); - let next_root_id = match bottom_root { - Root::Bottom => break, - Root::Tree(root_id) => self.store.next_root_id(root_id).await?, - }; - if let Some(next_root_id) = next_root_id { - let next_tree = self.store.tree(&next_root_id).await?; - blocks.append(self.layout_tree(context, frame, next_tree).await); - } else { - blocks.append(self.layout_bottom(context, frame).await); - } - } - - Ok(()) - } - - async fn fill_screen_and_clamp_scrolling( - &self, - context: &Context, - frame: &mut Frame, - blocks: &mut TreeBlocks, - ) -> Result<(), S::Error> { - let top_line = 0; - let bottom_line = frame.size().height as i32 - 1; - - self.expand_to_top(context, frame, blocks).await?; - - if blocks.blocks().top_line > top_line { - blocks.blocks_mut().set_top_line(0); - } - - self.expand_to_bottom(context, frame, blocks).await?; - - if blocks.blocks().bottom_line < bottom_line { - blocks.blocks_mut().set_bottom_line(bottom_line); - } - - self.expand_to_top(context, frame, blocks).await?; - - Ok(()) - } - - async fn layout_last_cursor_seed( - &self, - context: &Context, - frame: &mut Frame, - last_cursor_path: &Path, - ) -> Result, S::Error> { - Ok(match &self.last_cursor { - Cursor::Bottom => { - let mut blocks = self.layout_bottom(context, frame).await; - - let bottom_line = frame.size().height as i32 - 1; - blocks.blocks_mut().set_bottom_line(bottom_line); - - blocks - } - Cursor::Editor { parent: None, .. } | Cursor::Pseudo { parent: None, .. } => { - let mut blocks = self.layout_bottom(context, frame).await; - - blocks - .blocks_mut() - .recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line); - - blocks - } - Cursor::Msg(_) - | Cursor::Editor { - parent: Some(_), .. - } - | Cursor::Pseudo { - parent: Some(_), .. - } => { - let root = last_cursor_path.first(); - let tree = self.store.tree(root).await?; - let mut blocks = self.layout_tree(context, frame, tree).await; - - blocks - .blocks_mut() - .recalculate_offsets(&BlockId::LastCursor, self.last_cursor_line); - - blocks - } - }) - } - - async fn layout_cursor_seed( - &self, - context: &Context, - frame: &mut Frame, - last_cursor_path: &Path, - cursor_path: &Path, - ) -> Result, S::Error> { - let bottom_line = frame.size().height as i32 - 1; - - Ok(match &self.cursor { - Cursor::Bottom - | Cursor::Editor { parent: None, .. } - | Cursor::Pseudo { parent: None, .. } => { - let mut blocks = self.layout_bottom(context, frame).await; - - blocks.blocks_mut().set_bottom_line(bottom_line); - - blocks - } - Cursor::Msg(_) - | Cursor::Editor { - parent: Some(_), .. - } - | Cursor::Pseudo { - parent: Some(_), .. - } => { - let root = cursor_path.first(); - let tree = self.store.tree(root).await?; - let mut blocks = self.layout_tree(context, frame, tree).await; - - 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, - context: &Context, - frame: &mut Frame, - last_cursor_path: &Path, - cursor_path: &Path, - ) -> Result, S::Error> { - if let Cursor::Bottom = self.cursor { - self.layout_cursor_seed(context, frame, last_cursor_path, cursor_path) - .await - } else { - self.layout_last_cursor_seed(context, frame, last_cursor_path) - .await - } - } - - fn scroll_so_cursor_is_visible(&self, frame: &Frame, blocks: &mut TreeBlocks) { - if matches!(self.cursor, Cursor::Bottom) { - return; // Cursor is locked to bottom - } - - let block = blocks - .blocks() - .find(&BlockId::from_cursor(&self.cursor)) - .expect("no cursor found"); - - let height = frame.size().height as i32; - let scrolloff = scrolloff(height); - - let min_line = -block.focus.start + scrolloff; - let max_line = height - block.focus.end - scrolloff; - - // If the message is higher than the available space, the top of the - // message should always be visible. I'm not using top_line.clamp(...) - // because the order of the min and max matters. - let top_line = block.top_line; - #[allow(clippy::manual_clamp)] - let new_top_line = top_line.min(max_line).max(min_line); - if new_top_line != top_line { - blocks.blocks_mut().offset(new_top_line - top_line); - } - } - - fn scroll_so_cursor_is_centered(&self, frame: &Frame, blocks: &mut TreeBlocks) { - if matches!(self.cursor, Cursor::Bottom) { - return; // Cursor is locked to bottom - } - - let block = blocks - .blocks() - .find(&BlockId::from_cursor(&self.cursor)) - .expect("no cursor found"); - - let height = frame.size().height as i32; - let scrolloff = scrolloff(height); - - let min_line = -block.focus.start + scrolloff; - let max_line = height - block.focus.end - scrolloff; - - // If the message is higher than the available space, the top of the - // message should always be visible. I'm not using top_line.clamp(...) - // because the order of the min and max matters. - let top_line = block.top_line; - let new_top_line = (height - block.height) / 2; - #[allow(clippy::manual_clamp)] - let new_top_line = new_top_line.min(max_line).max(min_line); - if new_top_line != top_line { - blocks.blocks_mut().offset(new_top_line - top_line); - } - } - - /// Try to obtain a [`Cursor::Msg`] pointing to the block. - fn msg_id(block: &Block>) -> Option { - match &block.id { - BlockId::Msg(id) => Some(id.clone()), - _ => None, - } - } - - fn visible(block: &Block>, first_line: i32, last_line: i32) -> bool { - (first_line + 1 - block.height..=last_line).contains(&block.top_line) - } - - fn move_cursor_so_it_is_visible( - &mut self, - frame: &Frame, - blocks: &TreeBlocks, - ) -> Option { - if !matches!(self.cursor, Cursor::Bottom | Cursor::Msg(_)) { - // In all other cases, there is no need to make the cursor visible - // since scrolling behaves differently enough. - return None; - } - - let height = frame.size().height as i32; - let scrolloff = scrolloff(height); - - let first_line = scrolloff; - let last_line = height - 1 - scrolloff; - - let new_cursor = if matches!(self.cursor, Cursor::Bottom) { - blocks - .blocks() - .iter() - .rev() - .filter(|b| Self::visible(b, first_line, last_line)) - .find_map(Self::msg_id) - } else { - let block = blocks - .blocks() - .find(&BlockId::from_cursor(&self.cursor)) - .expect("no cursor found"); - - if Self::visible(block, first_line, last_line) { - return None; - } else if block.top_line < first_line { - blocks - .blocks() - .iter() - .filter(|b| Self::visible(b, first_line, last_line)) - .find_map(Self::msg_id) - } else { - blocks - .blocks() - .iter() - .rev() - .filter(|b| Self::visible(b, first_line, last_line)) - .find_map(Self::msg_id) - } - }; - - if let Some(id) = new_cursor { - self.cursor = Cursor::Msg(id.clone()); - Some(id) - } else { - None - } - } - - fn visible_msgs(frame: &Frame, blocks: &TreeBlocks) -> Vec { - let height: i32 = frame.size().height.into(); - let first_line = 0; - let last_line = first_line + height - 1; - - let mut result = vec![]; - for block in blocks.blocks().iter() { - if Self::visible(block, first_line, last_line) { - if let BlockId::Msg(id) = &block.id { - result.push(id.clone()); - } - } - } - - result - } - - pub async fn relayout( - &mut self, - nick: String, - focused: bool, - frame: &mut Frame, - ) -> Result, S::Error> { - // The basic idea is this: - // - // First, layout a full screen of blocks around self.last_cursor, using - // self.last_cursor_line for offset positioning. At this point, any - // outstanding scrolling is performed as well. - // - // 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 context = Context { nick, focused }; - - 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(&context, frame, &last_cursor_path, &cursor_path) - .await?; - blocks.blocks_mut().offset(self.scroll); - self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks) - .await?; - - if !self.contains_cursor(&blocks) { - blocks = self - .layout_cursor_seed(&context, frame, &last_cursor_path, &cursor_path) - .await?; - self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks) - .await?; - } - - match self.correction { - Some(Correction::MakeCursorVisible) => { - self.scroll_so_cursor_is_visible(frame, &mut blocks); - self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks) - .await?; - } - Some(Correction::MoveCursorToVisibleArea) => { - let new_cursor_msg_id = self.move_cursor_so_it_is_visible(frame, &blocks); - if let Some(cursor_msg_id) = new_cursor_msg_id { - // Moving the cursor invalidates our current blocks, so we sadly - // have to either perform an expensive operation or redraw the - // entire thing. I'm choosing the latter for now. - - self.last_cursor = self.cursor.clone(); - self.last_cursor_line = self.cursor_line(&blocks); - self.last_visible_msgs = Self::visible_msgs(frame, &blocks); - self.scroll = 0; - self.correction = None; - - let last_cursor_path = self.store.path(&cursor_msg_id).await?; - blocks = self - .layout_last_cursor_seed(&context, frame, &last_cursor_path) - .await?; - self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks) - .await?; - } - } - Some(Correction::CenterCursor) => { - self.scroll_so_cursor_is_centered(frame, &mut blocks); - self.fill_screen_and_clamp_scrolling(&context, frame, &mut blocks) - .await?; - } - None => {} - } - - self.last_cursor = self.cursor.clone(); - self.last_cursor_line = self.cursor_line(&blocks); - self.last_visible_msgs = Self::visible_msgs(frame, &blocks); - self.scroll = 0; - self.correction = None; - - Ok(blocks) - } -} diff --git a/src/ui/chat/tree/tree_blocks.rs b/src/ui/chat/tree/tree_blocks.rs deleted file mode 100644 index 69b98ec..0000000 --- a/src/ui/chat/tree/tree_blocks.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::ui::chat::blocks::Blocks; - -use super::Cursor; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BlockId { - Msg(I), - Cursor, - LastCursor, -} - -impl BlockId { - pub fn from_cursor(cursor: &Cursor) -> Self { - match cursor { - Cursor::Msg(id) => Self::Msg(id.clone()), - _ => Self::Cursor, - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum Root { - Bottom, - Tree(I), -} - -pub struct TreeBlocks { - blocks: Blocks>, - top_root: Root, - bottom_root: Root, -} - -impl TreeBlocks { - pub fn new(top_root: Root, bottom_root: Root) -> Self { - Self { - blocks: Blocks::new(), - top_root, - bottom_root, - } - } - - pub fn blocks(&self) -> &Blocks> { - &self.blocks - } - - pub fn blocks_mut(&mut self) -> &mut Blocks> { - &mut self.blocks - } - - pub fn into_blocks(self) -> Blocks> { - self.blocks - } - - pub fn top_root(&self) -> &Root { - &self.top_root - } - - pub fn bottom_root(&self) -> &Root { - &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; - } -} diff --git a/src/ui/chat/tree/widgets.rs b/src/ui/chat/tree/widgets.rs deleted file mode 100644 index 919d98b..0000000 --- a/src/ui/chat/tree/widgets.rs +++ /dev/null @@ -1,165 +0,0 @@ -mod indent; -mod seen; -mod time; - -use crossterm::style::Stylize; -use toss::{Style, Styled, WidthDb}; - -use super::super::ChatMsg; -use crate::store::Msg; -use crate::ui::widgets::editor::EditorState; -use crate::ui::widgets::join::{HJoin, Segment}; -use crate::ui::widgets::layer::Layer; -use crate::ui::widgets::padding::Padding; -use crate::ui::widgets::text::Text; -use crate::ui::widgets::BoxedWidget; - -use self::indent::Indent; - -pub const PLACEHOLDER: &str = "[...]"; - -pub fn style_placeholder() -> Style { - Style::new().dark_grey() -} - -fn style_time(highlighted: bool) -> Style { - if highlighted { - Style::new().black().on_white() - } else { - Style::new().grey() - } -} - -fn style_indent(highlighted: bool) -> Style { - if highlighted { - Style::new().black().on_white() - } else { - Style::new().dark_grey() - } -} - -fn style_info() -> Style { - Style::new().italic().dark_grey() -} - -fn style_editor_highlight() -> Style { - Style::new().black().on_cyan() -} - -fn style_pseudo_highlight() -> Style { - Style::new().black().on_yellow() -} - -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( - Padding::new(time::widget(Some(msg.time()), style_time(highlighted))) - .stretch(true) - .right(1), - ), - Segment::new(Indent::new(indent, style_indent(highlighted))), - Segment::new(Layer::new(vec![ - Padding::new(Indent::new(1, style_indent(false))) - .top(1) - .into(), - Padding::new(Text::new(nick)).right(1).into(), - ])), - // TODO Minimum content width - // TODO Minimizing and maximizing messages - Segment::new(Text::new(content).wrap(true)).priority(1), - ]) - .into() -} - -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( - Padding::new(time::widget(None, style_time(highlighted))) - .stretch(true) - .right(1), - ), - Segment::new(Indent::new(indent, style_indent(highlighted))), - Segment::new(Text::new(content)), - ]) - .into() -} - -pub fn editor( - widthdb: &mut WidthDb, - indent: usize, - nick: &str, - editor: &EditorState, -) -> (BoxedWidget, usize) { - let (nick, content) = M::edit(nick, &editor.text()); - let editor = editor.widget().highlight(|_| content); - let cursor_row = editor.cursor_row(widthdb); - - let widget = HJoin::new(vec![ - Segment::new(seen::widget(true)), - Segment::new( - Padding::new(time::widget(None, style_editor_highlight())) - .stretch(true) - .right(1), - ), - Segment::new(Indent::new(indent, style_editor_highlight())), - Segment::new(Layer::new(vec![ - Padding::new(Indent::new(1, style_indent(false))) - .top(1) - .into(), - Padding::new(Text::new(nick)).right(1).into(), - ])), - Segment::new(editor).priority(1).expanding(true), - ]) - .into(); - - (widget, cursor_row) -} - -pub fn pseudo(indent: usize, nick: &str, editor: &EditorState) -> BoxedWidget { - let (nick, content) = M::edit(nick, &editor.text()); - - HJoin::new(vec![ - Segment::new(seen::widget(true)), - Segment::new( - Padding::new(time::widget(None, style_pseudo_highlight())) - .stretch(true) - .right(1), - ), - Segment::new(Indent::new(indent, style_pseudo_highlight())), - Segment::new(Layer::new(vec![ - Padding::new(Indent::new(1, style_indent(false))) - .top(1) - .into(), - Padding::new(Text::new(nick)).right(1).into(), - ])), - Segment::new(Text::new(content).wrap(true)).priority(1), - ]) - .into() -} diff --git a/src/ui/chat/tree/widgets/indent.rs b/src/ui/chat/tree/widgets/indent.rs deleted file mode 100644 index a226f93..0000000 --- a/src/ui/chat/tree/widgets/indent.rs +++ /dev/null @@ -1,41 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, Style, WidthDb}; - -use crate::ui::widgets::Widget; - -pub const INDENT: &str = "│ "; -pub const INDENT_WIDTH: usize = 2; - -pub struct Indent { - level: usize, - style: Style, -} - -impl Indent { - pub fn new(level: usize, style: Style) -> Self { - Self { level, style } - } -} - -#[async_trait] -impl Widget for Indent { - async fn size( - &self, - _widthdb: &mut WidthDb, - _max_width: Option, - _max_height: Option, - ) -> Size { - Size::new((INDENT_WIDTH * self.level) as u16, 0) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - - for y in 0..size.height { - frame.write( - Pos::new(0, y.into()), - (INDENT.repeat(self.level), self.style), - ) - } - } -} diff --git a/src/ui/chat/tree/widgets/seen.rs b/src/ui/chat/tree/widgets/seen.rs deleted file mode 100644 index d53271b..0000000 --- a/src/ui/chat/tree/widgets/seen.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crossterm::style::Stylize; -use toss::Style; - -use crate::ui::widgets::background::Background; -use crate::ui::widgets::empty::Empty; -use crate::ui::widgets::text::Text; -use crate::ui::widgets::BoxedWidget; - -const UNSEEN: &str = "*"; -const WIDTH: u16 = 1; - -fn seen_style() -> Style { - Style::new().black().on_green() -} - -pub fn widget(seen: bool) -> BoxedWidget { - if seen { - Empty::new().width(WIDTH).into() - } else { - let style = seen_style(); - Background::new(Text::new((UNSEEN, style))) - .style(style) - .into() - } -} diff --git a/src/ui/chat/tree/widgets/time.rs b/src/ui/chat/tree/widgets/time.rs deleted file mode 100644 index 0801126..0000000 --- a/src/ui/chat/tree/widgets/time.rs +++ /dev/null @@ -1,25 +0,0 @@ -use time::format_description::FormatItem; -use time::macros::format_description; -use time::OffsetDateTime; -use toss::Style; - -use crate::ui::widgets::background::Background; -use crate::ui::widgets::empty::Empty; -use crate::ui::widgets::text::Text; -use crate::ui::widgets::BoxedWidget; - -const TIME_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]"); -const TIME_WIDTH: u16 = 16; - -pub fn widget(time: Option, style: Style) -> BoxedWidget { - if let Some(time) = time { - let text = time.format(TIME_FORMAT).expect("could not format time"); - Background::new(Text::new((text, style))) - .style(style) - .into() - } else { - Background::new(Empty::new().width(TIME_WIDTH)) - .style(style) - .into() - } -} diff --git a/src/ui/util.rs b/src/ui/util.rs deleted file mode 100644 index 2ba5241..0000000 --- a/src/ui/util.rs +++ /dev/null @@ -1,166 +0,0 @@ -use std::io; -use std::sync::Arc; - -use parking_lot::FairMutex; -use toss::Terminal; - -use super::input::{key, InputEvent, KeyBindingsList}; -use super::widgets::editor::EditorState; -use super::widgets::list::ListState; - -pub fn prompt( - terminal: &mut Terminal, - crossterm_lock: &Arc>, - initial_text: &str, -) -> io::Result { - let content = { - let _guard = crossterm_lock.lock(); - terminal.suspend().expect("could not suspend"); - let content = edit::edit(initial_text); - terminal.unsuspend().expect("could not unsuspend"); - content - }; - - content -} - -////////// -// List // -////////// - -pub fn list_list_key_bindings(bindings: &mut KeyBindingsList) { - bindings.binding("j/k, ↓/↑", "move cursor up/down"); - bindings.binding("g, home", "move cursor to top"); - bindings.binding("G, end", "move cursor to bottom"); - bindings.binding("ctrl+y/e", "scroll up/down"); -} - -pub fn handle_list_input_event(list: &mut ListState, event: &InputEvent) -> bool { - match event { - key!('k') | key!(Up) => list.move_cursor_up(), - key!('j') | key!(Down) => list.move_cursor_down(), - key!('g') | key!(Home) => list.move_cursor_to_top(), - key!('G') | key!(End) => list.move_cursor_to_bottom(), - key!(Ctrl + 'y') => list.scroll_up(1), - key!(Ctrl + 'e') => list.scroll_down(1), - _ => return false, - } - - true -} - -//////////// -// Editor // -//////////// - -fn list_editor_editing_key_bindings( - bindings: &mut KeyBindingsList, - char_filter: impl Fn(char) -> bool, -) { - if char_filter('\n') { - bindings.binding("enter+", "insert newline"); - } - - bindings.binding("ctrl+h, backspace", "delete before cursor"); - bindings.binding("ctrl+d, delete", "delete after cursor"); - bindings.binding("ctrl+l", "clear editor contents"); -} - -fn list_editor_cursor_movement_key_bindings(bindings: &mut KeyBindingsList) { - bindings.binding("ctrl+b, ←", "move cursor left"); - bindings.binding("ctrl+f, →", "move cursor right"); - bindings.binding("alt+b, ctrl+←", "move cursor left a word"); - bindings.binding("alt+f, ctrl+→", "move cursor right a word"); - bindings.binding("ctrl+a, home", "move cursor to start of line"); - bindings.binding("ctrl+e, end", "move cursor to end of line"); - bindings.binding("↑/↓", "move cursor up/down"); -} - -pub fn list_editor_key_bindings( - bindings: &mut KeyBindingsList, - char_filter: impl Fn(char) -> bool, -) { - list_editor_editing_key_bindings(bindings, char_filter); - bindings.empty(); - list_editor_cursor_movement_key_bindings(bindings); -} - -pub fn handle_editor_input_event( - editor: &EditorState, - terminal: &mut Terminal, - event: &InputEvent, - char_filter: impl Fn(char) -> bool, -) -> bool { - match event { - // Enter with *any* modifier pressed - if ctrl and shift don't - // work, maybe alt does - key!(Enter) => return false, - InputEvent::Key(crate::ui::input::KeyEvent { - code: crossterm::event::KeyCode::Enter, - .. - }) if char_filter('\n') => editor.insert_char(terminal.widthdb(), '\n'), - - // Editing - key!(Char ch) if char_filter(*ch) => editor.insert_char(terminal.widthdb(), *ch), - key!(Paste str) => { - // It seems that when pasting, '\n' are converted into '\r' for some - // reason. I don't really know why, or at what point this happens. - // Vim converts any '\r' pasted via the terminal into '\n', so I - // decided to mirror that behaviour. - let str = str.replace('\r', "\n"); - if str.chars().all(char_filter) { - editor.insert_str(terminal.widthdb(), &str); - } else { - return false; - } - } - key!(Ctrl + 'h') | key!(Backspace) => editor.backspace(terminal.widthdb()), - key!(Ctrl + 'd') | key!(Delete) => editor.delete(), - key!(Ctrl + 'l') => editor.clear(), - // TODO Key bindings to delete words - - // Cursor movement - key!(Ctrl + 'b') | key!(Left) => editor.move_cursor_left(terminal.widthdb()), - key!(Ctrl + 'f') | key!(Right) => editor.move_cursor_right(terminal.widthdb()), - key!(Alt + 'b') | key!(Ctrl + Left) => editor.move_cursor_left_a_word(terminal.widthdb()), - key!(Alt + 'f') | key!(Ctrl + Right) => editor.move_cursor_right_a_word(terminal.widthdb()), - key!(Ctrl + 'a') | key!(Home) => editor.move_cursor_to_start_of_line(terminal.widthdb()), - key!(Ctrl + 'e') | key!(End) => editor.move_cursor_to_end_of_line(terminal.widthdb()), - key!(Up) => editor.move_cursor_up(terminal.widthdb()), - key!(Down) => editor.move_cursor_down(terminal.widthdb()), - - _ => return false, - } - - true -} - -pub fn list_editor_key_bindings_allowing_external_editing( - bindings: &mut KeyBindingsList, - char_filter: impl Fn(char) -> bool, -) { - list_editor_editing_key_bindings(bindings, char_filter); - bindings.binding("ctrl+x", "edit in external editor"); - bindings.empty(); - list_editor_cursor_movement_key_bindings(bindings); -} - -pub fn handle_editor_input_event_allowing_external_editing( - editor: &EditorState, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - event: &InputEvent, - char_filter: impl Fn(char) -> bool, -) -> io::Result { - if let key!(Ctrl + 'x') = event { - editor.edit_externally(terminal, crossterm_lock)?; - Ok(true) - } else { - Ok(handle_editor_input_event( - editor, - terminal, - event, - char_filter, - )) - } -} diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs deleted file mode 100644 index 6f137b8..0000000 --- a/src/ui/widgets.rs +++ /dev/null @@ -1,109 +0,0 @@ -// Since the widget module is effectively a library and will probably be moved -// to toss later, warnings about unused functions are mostly inaccurate. -// TODO Restrict this a bit more? -#![allow(dead_code)] - -pub mod background; -pub mod border; -pub mod cursor; -pub mod editor; -pub mod empty; -pub mod float; -pub mod join; -pub mod layer; -pub mod list; -pub mod padding; -pub mod popup; -pub mod resize; -pub mod rules; -pub mod text; - -use async_trait::async_trait; -use toss::{AsyncWidget, Frame, Size, WidthDb}; - -use super::UiError; - -// TODO Add Error type and return Result-s (at least in Widget::render) - -#[async_trait] -pub trait Widget { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size; - - async fn render(self: Box, frame: &mut Frame); -} - -pub type BoxedWidget = Box; - -impl From for BoxedWidget { - fn from(widget: W) -> Self { - Box::new(widget) - } -} - -/// Wrapper that implements [`Widget`] for an [`AsyncWidget`]. -pub struct AsyncWidgetWrapper { - inner: I, -} - -impl AsyncWidgetWrapper { - pub fn new(inner: I) -> Self { - Self { inner } - } -} - -#[async_trait] -impl Widget for AsyncWidgetWrapper -where - I: AsyncWidget + Send + Sync, -{ - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - self.inner - .size(widthdb, max_width, max_height) - .await - .unwrap() - } - - async fn render(self: Box, frame: &mut Frame) { - self.inner.draw(frame).await.unwrap(); - } -} - -/// Wrapper that implements [`AsyncWidget`] for a [`Widget`]. -pub struct WidgetWrapper { - inner: BoxedWidget, -} - -impl WidgetWrapper { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - } - } -} - -#[async_trait] -impl AsyncWidget for WidgetWrapper { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Result { - Ok(self.inner.size(widthdb, max_width, max_height).await) - } - - async fn draw(self, frame: &mut Frame) -> Result<(), E> { - self.inner.render(frame).await; - Ok(()) - } -} diff --git a/src/ui/widgets/background.rs b/src/ui/widgets/background.rs deleted file mode 100644 index 432e54d..0000000 --- a/src/ui/widgets/background.rs +++ /dev/null @@ -1,46 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, Style, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Background { - inner: BoxedWidget, - style: Style, -} - -impl Background { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - style: Style::new().opaque(), - } - } - - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } -} - -#[async_trait] -impl Widget for Background { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - self.inner.size(widthdb, max_width, max_height).await - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - for dy in 0..size.height { - for dx in 0..size.width { - frame.write(Pos::new(dx.into(), dy.into()), (" ", self.style)); - } - } - - self.inner.render(frame).await; - } -} diff --git a/src/ui/widgets/border.rs b/src/ui/widgets/border.rs deleted file mode 100644 index bfd76ea..0000000 --- a/src/ui/widgets/border.rs +++ /dev/null @@ -1,65 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, Style, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Border { - inner: BoxedWidget, - style: Style, -} - -impl Border { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - style: Style::new(), - } - } - - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } -} - -#[async_trait] -impl Widget for Border { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - let max_width = max_width.map(|w| w.saturating_sub(2)); - let max_height = max_height.map(|h| h.saturating_sub(2)); - let size = self.inner.size(widthdb, max_width, max_height).await; - size + Size::new(2, 2) - } - - async fn render(self: Box, frame: &mut Frame) { - let mut size = frame.size(); - size.width = size.width.max(2); - size.height = size.height.max(2); - - let right = size.width as i32 - 1; - let bottom = size.height as i32 - 1; - frame.write(Pos::new(0, 0), ("┌", self.style)); - frame.write(Pos::new(right, 0), ("┐", self.style)); - frame.write(Pos::new(0, bottom), ("└", self.style)); - frame.write(Pos::new(right, bottom), ("┘", self.style)); - - for y in 1..bottom { - frame.write(Pos::new(0, y), ("│", self.style)); - frame.write(Pos::new(right, y), ("│", self.style)); - } - - for x in 1..right { - frame.write(Pos::new(x, 0), ("─", self.style)); - frame.write(Pos::new(x, bottom), ("─", self.style)); - } - - frame.push(Pos::new(1, 1), size - Size::new(2, 2)); - self.inner.render(frame).await; - frame.pop(); - } -} diff --git a/src/ui/widgets/cursor.rs b/src/ui/widgets/cursor.rs deleted file mode 100644 index 22ac5cc..0000000 --- a/src/ui/widgets/cursor.rs +++ /dev/null @@ -1,44 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Cursor { - inner: BoxedWidget, - pos: Pos, -} - -impl Cursor { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - pos: Pos::ZERO, - } - } - - pub fn at(mut self, pos: Pos) -> Self { - self.pos = pos; - self - } - - pub fn at_xy(self, x: i32, y: i32) -> Self { - self.at(Pos::new(x, y)) - } -} - -#[async_trait] -impl Widget for Cursor { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - self.inner.size(widthdb, max_width, max_height).await - } - - async fn render(self: Box, frame: &mut Frame) { - self.inner.render(frame).await; - frame.set_cursor(Some(self.pos)); - } -} diff --git a/src/ui/widgets/editor.rs b/src/ui/widgets/editor.rs deleted file mode 100644 index e2b8955..0000000 --- a/src/ui/widgets/editor.rs +++ /dev/null @@ -1,566 +0,0 @@ -use std::sync::Arc; -use std::{io, iter}; - -use async_trait::async_trait; -use crossterm::style::Stylize; -use parking_lot::{FairMutex, Mutex}; -use toss::{Frame, Pos, Size, Style, Styled, Terminal, WidthDb}; -use unicode_segmentation::UnicodeSegmentation; - -use crate::ui::util; - -use super::text::Text; -use super::Widget; - -/// Like [`WidthDb::wrap`] but includes a final break index if the text ends -/// with a newline. -fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec { - let mut breaks = widthdb.wrap(text, width); - if text.ends_with('\n') { - breaks.push(text.len()) - } - breaks -} - -/////////// -// State // -/////////// - -struct InnerEditorState { - text: String, - - /// Index of the cursor in the text. - /// - /// Must point to a valid grapheme boundary. - idx: usize, - - /// Column of the cursor on the screen just after it was last moved - /// horizontally. - col: usize, - - /// Width of the text when the editor was last rendered. - /// - /// Does not include additional column for cursor. - last_width: u16, -} - -impl InnerEditorState { - fn new(text: String) -> Self { - Self { - idx: text.len(), - col: 0, - last_width: u16::MAX, - text, - } - } - - /////////////////////////////// - // Grapheme helper functions // - /////////////////////////////// - - fn grapheme_boundaries(&self) -> Vec { - self.text - .grapheme_indices(true) - .map(|(i, _)| i) - .chain(iter::once(self.text.len())) - .collect() - } - - /// Ensure the cursor index lies on a grapheme boundary. If it doesn't, it - /// is moved to the next grapheme boundary. - /// - /// Can handle arbitrary cursor index. - fn move_cursor_to_grapheme_boundary(&mut self) { - for i in self.grapheme_boundaries() { - #[allow(clippy::comparison_chain)] - if i == self.idx { - // We're at a valid grapheme boundary already - return; - } else if i > self.idx { - // There was no valid grapheme boundary at our cursor index, so - // we'll take the next one we can get. - self.idx = i; - return; - } - } - - // The cursor was out of bounds, so move it to the last valid index. - self.idx = self.text.len(); - } - - /////////////////////////////// - // Line/col helper functions // - /////////////////////////////// - - /// Like [`Self::grapheme_boundaries`] but for lines. - /// - /// Note that the last line can have a length of 0 if the text ends with a - /// newline. - fn line_boundaries(&self) -> Vec { - let newlines = self - .text - .char_indices() - .filter(|(_, c)| *c == '\n') - .map(|(i, _)| i + 1); // utf-8 encodes '\n' as a single byte - iter::once(0) - .chain(newlines) - .chain(iter::once(self.text.len())) - .collect() - } - - /// Find the cursor's current line. - /// - /// Returns `(line_nr, start_idx, end_idx)`. - fn cursor_line(&self, boundaries: &[usize]) -> (usize, usize, usize) { - let mut result = (0, 0, 0); - for (i, (start, end)) in boundaries.iter().zip(boundaries.iter().skip(1)).enumerate() { - if self.idx >= *start { - result = (i, *start, *end); - } else { - break; - } - } - result - } - - fn cursor_col(&self, widthdb: &mut WidthDb, line_start: usize) -> usize { - widthdb.width(&self.text[line_start..self.idx]) - } - - fn line(&self, line: usize) -> (usize, usize) { - let boundaries = self.line_boundaries(); - boundaries - .iter() - .copied() - .zip(boundaries.iter().copied().skip(1)) - .nth(line) - .expect("line exists") - } - - fn move_cursor_to_line_col(&mut self, widthdb: &mut WidthDb, line: usize, col: usize) { - let (start, end) = self.line(line); - let line = &self.text[start..end]; - - let mut width = 0; - for (gi, g) in line.grapheme_indices(true) { - self.idx = start + gi; - if col > width { - width += widthdb.grapheme_width(g, width) as usize; - } else { - return; - } - } - - if !line.ends_with('\n') { - self.idx = end; - } - } - - fn record_cursor_col(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.line_boundaries(); - let (_, start, _) = self.cursor_line(&boundaries); - self.col = self.cursor_col(widthdb, start); - } - - ///////////// - // Editing // - ///////////// - - fn clear(&mut self) { - self.text = String::new(); - self.idx = 0; - self.col = 0; - } - - fn set_text(&mut self, widthdb: &mut WidthDb, text: String) { - self.text = text; - self.move_cursor_to_grapheme_boundary(); - self.record_cursor_col(widthdb); - } - - /// Insert a character at the current cursor position and move the cursor - /// accordingly. - fn insert_char(&mut self, widthdb: &mut WidthDb, ch: char) { - self.text.insert(self.idx, ch); - self.idx += ch.len_utf8(); - self.record_cursor_col(widthdb); - } - - /// Insert a string at the current cursor position and move the cursor - /// accordingly. - fn insert_str(&mut self, widthdb: &mut WidthDb, str: &str) { - self.text.insert_str(self.idx, str); - self.idx += str.len(); - self.record_cursor_col(widthdb); - } - - /// Delete the grapheme before the cursor position. - fn backspace(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.grapheme_boundaries(); - for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { - if *end == self.idx { - self.text.replace_range(start..end, ""); - self.idx = *start; - self.record_cursor_col(widthdb); - break; - } - } - } - - /// Delete the grapheme after the cursor position. - fn delete(&mut self) { - let boundaries = self.grapheme_boundaries(); - for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { - if *start == self.idx { - self.text.replace_range(start..end, ""); - break; - } - } - } - - ///////////////////// - // Cursor movement // - ///////////////////// - - fn move_cursor_left(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.grapheme_boundaries(); - for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { - if *end == self.idx { - self.idx = *start; - self.record_cursor_col(widthdb); - break; - } - } - } - - fn move_cursor_right(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.grapheme_boundaries(); - for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { - if *start == self.idx { - self.idx = *end; - self.record_cursor_col(widthdb); - break; - } - } - } - - fn move_cursor_left_a_word(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.grapheme_boundaries(); - let mut encountered_word = false; - for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)).rev() { - if *end == self.idx { - let g = &self.text[*start..*end]; - let whitespace = g.chars().all(|c| c.is_whitespace()); - if encountered_word && whitespace { - break; - } else if !whitespace { - encountered_word = true; - } - self.idx = *start; - } - } - self.record_cursor_col(widthdb); - } - - fn move_cursor_right_a_word(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.grapheme_boundaries(); - let mut encountered_word = false; - for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { - if *start == self.idx { - let g = &self.text[*start..*end]; - let whitespace = g.chars().all(|c| c.is_whitespace()); - if encountered_word && whitespace { - break; - } else if !whitespace { - encountered_word = true; - } - self.idx = *end; - } - } - self.record_cursor_col(widthdb); - } - - fn move_cursor_to_start_of_line(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.line_boundaries(); - let (line, _, _) = self.cursor_line(&boundaries); - self.move_cursor_to_line_col(widthdb, line, 0); - self.record_cursor_col(widthdb); - } - - fn move_cursor_to_end_of_line(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.line_boundaries(); - let (line, _, _) = self.cursor_line(&boundaries); - self.move_cursor_to_line_col(widthdb, line, usize::MAX); - self.record_cursor_col(widthdb); - } - - fn move_cursor_up(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.line_boundaries(); - let (line, _, _) = self.cursor_line(&boundaries); - if line > 0 { - self.move_cursor_to_line_col(widthdb, line - 1, self.col); - } - } - - fn move_cursor_down(&mut self, widthdb: &mut WidthDb) { - let boundaries = self.line_boundaries(); - - // There's always at least one line, and always at least two line - // boundaries at 0 and self.text.len(). - let amount_of_lines = boundaries.len() - 1; - - let (line, _, _) = self.cursor_line(&boundaries); - if line + 1 < amount_of_lines { - self.move_cursor_to_line_col(widthdb, line + 1, self.col); - } - } -} - -pub struct EditorState(Arc>); - -impl EditorState { - pub fn new() -> Self { - Self(Arc::new(Mutex::new(InnerEditorState::new(String::new())))) - } - - pub fn with_initial_text(text: String) -> Self { - Self(Arc::new(Mutex::new(InnerEditorState::new(text)))) - } - - pub fn widget(&self) -> Editor { - let guard = self.0.lock(); - let text = Styled::new_plain(guard.text.clone()); - let idx = guard.idx; - Editor { - state: self.0.clone(), - text, - idx, - focus: true, - hidden: None, - } - } - - pub fn text(&self) -> String { - self.0.lock().text.clone() - } - - pub fn clear(&self) { - self.0.lock().clear(); - } - - pub fn set_text(&self, widthdb: &mut WidthDb, text: String) { - self.0.lock().set_text(widthdb, text); - } - - pub fn insert_char(&self, widthdb: &mut WidthDb, ch: char) { - self.0.lock().insert_char(widthdb, ch); - } - - pub fn insert_str(&self, widthdb: &mut WidthDb, str: &str) { - self.0.lock().insert_str(widthdb, str); - } - - /// Delete the grapheme before the cursor position. - pub fn backspace(&self, widthdb: &mut WidthDb) { - self.0.lock().backspace(widthdb); - } - - /// Delete the grapheme after the cursor position. - pub fn delete(&self) { - self.0.lock().delete(); - } - - pub fn move_cursor_left(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_left(widthdb); - } - - pub fn move_cursor_right(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_right(widthdb); - } - - pub fn move_cursor_left_a_word(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_left_a_word(widthdb); - } - - pub fn move_cursor_right_a_word(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_right_a_word(widthdb); - } - - pub fn move_cursor_to_start_of_line(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_to_start_of_line(widthdb); - } - - pub fn move_cursor_to_end_of_line(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_to_end_of_line(widthdb); - } - - pub fn move_cursor_up(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_up(widthdb); - } - - pub fn move_cursor_down(&self, widthdb: &mut WidthDb) { - self.0.lock().move_cursor_down(widthdb); - } - - pub fn edit_externally( - &self, - terminal: &mut Terminal, - crossterm_lock: &Arc>, - ) -> io::Result<()> { - let mut guard = self.0.lock(); - let text = util::prompt(terminal, crossterm_lock, &guard.text)?; - - if text.trim().is_empty() { - // The user likely wanted to abort the edit and has deleted the - // entire text (bar whitespace left over by some editors). - return Ok(()); - } - - if let Some(text) = text.strip_suffix('\n') { - // Some editors like vim add a trailing newline that would look out - // of place in cove's editor. To intentionally add a trailing - // newline, simply add two in-editor. - guard.set_text(terminal.widthdb(), text.to_string()); - } else { - guard.set_text(terminal.widthdb(), text); - } - - Ok(()) - } -} - -//////////// -// Widget // -//////////// - -pub struct Editor { - state: Arc>, - text: Styled, - idx: usize, - focus: bool, - hidden: Option>, -} - -impl Editor { - pub fn highlight(mut self, f: F) -> Self - where - F: FnOnce(&str) -> Styled, - { - let new_text = f(self.text.text()); - assert_eq!(self.text.text(), new_text.text()); - self.text = new_text; - self - } - - pub fn focus(mut self, active: bool) -> Self { - self.focus = active; - self - } - - pub fn hidden(self) -> Self { - self.hidden_with_placeholder(("", Style::new().grey().italic())) - } - - pub fn hidden_with_placeholder>(mut self, placeholder: S) -> Self { - self.hidden = Some(Box::new(Text::new(placeholder))); - self - } - - fn wrapped_cursor(cursor_idx: usize, break_indices: &[usize]) -> (usize, usize) { - let mut row = 0; - let mut line_idx = cursor_idx; - - for break_idx in break_indices { - if cursor_idx < *break_idx { - break; - } else { - row += 1; - line_idx = cursor_idx - break_idx; - } - } - - (row, line_idx) - } - - pub fn cursor_row(&self, widthdb: &mut WidthDb) -> usize { - let width = self.state.lock().last_width; - let text_width = (width - 1) as usize; - let indices = wrap(widthdb, self.text.text(), text_width); - let (row, _) = Self::wrapped_cursor(self.idx, &indices); - row - } -} - -#[async_trait] -impl Widget for Editor { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - if let Some(placeholder) = &self.hidden { - let mut size = placeholder.size(widthdb, max_width, max_height).await; - - // Cursor needs to fit regardless of focus - size.width = size.width.max(1); - size.height = size.height.max(1); - - return size; - } - - let max_width = max_width.map(|w| w as usize).unwrap_or(usize::MAX).max(1); - let max_text_width = max_width - 1; - let indices = wrap(widthdb, self.text.text(), max_text_width); - let lines = self.text.clone().split_at_indices(&indices); - - let min_width = lines - .iter() - .map(|l| widthdb.width(l.text().trim_end())) - .max() - .unwrap_or(0) - + 1; - let min_height = lines.len(); - Size::new(min_width as u16, min_height as u16) - } - - async fn render(self: Box, frame: &mut Frame) { - if let Some(placeholder) = self.hidden { - if !self.text.text().is_empty() { - placeholder.render(frame).await; - } - if self.focus { - frame.set_cursor(Some(Pos::ZERO)); - } - return; - } - - let size = frame.size(); - let widthdb = frame.widthdb(); - - let width = size.width.max(1); - let text_width = (width - 1) as usize; - let indices = wrap(widthdb, self.text.text(), text_width); - let lines = self.text.split_at_indices(&indices); - - // Determine cursor position now while we still have the lines. - let cursor_pos = if self.focus { - let (cursor_row, cursor_line_idx) = Self::wrapped_cursor(self.idx, &indices); - let cursor_col = widthdb.width(lines[cursor_row].text().split_at(cursor_line_idx).0); - let cursor_col = cursor_col.min(text_width); - Some(Pos::new(cursor_col as i32, cursor_row as i32)) - } else { - None - }; - - for (i, line) in lines.into_iter().enumerate() { - frame.write(Pos::new(0, i as i32), line); - } - - if let Some(pos) = cursor_pos { - frame.set_cursor(Some(pos)); - } - - self.state.lock().last_width = width; - } -} diff --git a/src/ui/widgets/empty.rs b/src/ui/widgets/empty.rs deleted file mode 100644 index a5d98ea..0000000 --- a/src/ui/widgets/empty.rs +++ /dev/null @@ -1,44 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Size, WidthDb}; - -use super::Widget; - -#[derive(Debug, Default, Clone, Copy)] -pub struct Empty { - size: Size, -} - -impl Empty { - pub fn new() -> Self { - Self { size: Size::ZERO } - } - - pub fn width(mut self, width: u16) -> Self { - self.size.width = width; - self - } - - pub fn height(mut self, height: u16) -> Self { - self.size.height = height; - self - } - - pub fn size(mut self, size: Size) -> Self { - self.size = size; - self - } -} - -#[async_trait] -impl Widget for Empty { - async fn size( - &self, - _widthdb: &mut WidthDb, - _max_width: Option, - _max_height: Option, - ) -> Size { - self.size - } - - async fn render(self: Box, _frame: &mut Frame) {} -} diff --git a/src/ui/widgets/float.rs b/src/ui/widgets/float.rs deleted file mode 100644 index a262cd6..0000000 --- a/src/ui/widgets/float.rs +++ /dev/null @@ -1,73 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Float { - inner: BoxedWidget, - horizontal: Option, - vertical: Option, -} - -impl Float { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - horizontal: None, - vertical: None, - } - } - - pub fn horizontal(mut self, position: f32) -> Self { - self.horizontal = Some(position); - self - } - - pub fn vertical(mut self, position: f32) -> Self { - self.vertical = Some(position); - self - } -} - -#[async_trait] -impl Widget for Float { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - self.inner.size(widthdb, max_width, max_height).await - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - - let mut inner_size = self - .inner - .size(frame.widthdb(), Some(size.width), Some(size.height)) - .await; - inner_size.width = inner_size.width.min(size.width); - inner_size.height = inner_size.height.min(size.height); - - let mut inner_pos = Pos::ZERO; - - if let Some(horizontal) = self.horizontal { - let available = (size.width - inner_size.width) as f32; - // Biased towards the left if horizontal lands exactly on the - // boundary between two cells - inner_pos.x = (horizontal * available).floor().min(available) as i32; - } - - if let Some(vertical) = self.vertical { - let available = (size.height - inner_size.height) as f32; - // Biased towards the top if vertical lands exactly on the boundary - // between two cells - inner_pos.y = (vertical * available).floor().min(available) as i32; - } - - frame.push(inner_pos, inner_size); - self.inner.render(frame).await; - frame.pop(); - } -} diff --git a/src/ui/widgets/join.rs b/src/ui/widgets/join.rs deleted file mode 100644 index 2aa4551..0000000 --- a/src/ui/widgets/join.rs +++ /dev/null @@ -1,265 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Segment { - widget: BoxedWidget, - expanding: bool, - priority: Option, -} - -impl Segment { - pub fn new>(widget: W) -> Self { - Self { - widget: widget.into(), - expanding: false, - priority: None, - } - } - - /// Expand this segment into the remaining space after all segment minimum - /// sizes have been determined. The remaining space is split up evenly. - pub fn expanding(mut self, active: bool) -> Self { - self.expanding = active; - self - } - - /// The size of segments with a priority is calculated in order of - /// increasing priority, using the remaining available space as maximum - /// space for the widget during size calculations. - /// - /// Widgets without priority are processed first without size restrictions. - pub fn priority(mut self, priority: u8) -> Self { - self.priority = Some(priority); - self - } -} - -struct SizedSegment { - idx: usize, - size: Size, - expanding: bool, - priority: Option, -} - -impl SizedSegment { - pub fn new(idx: usize, segment: &Segment) -> Self { - Self { - idx, - size: Size::ZERO, - expanding: segment.expanding, - priority: segment.priority, - } - } -} - -async fn sizes_horiz( - segments: &[Segment], - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, -) -> Vec { - let mut sized = segments - .iter() - .enumerate() - .map(|(i, s)| SizedSegment::new(i, s)) - .collect::>(); - sized.sort_by_key(|s| s.priority); - - let mut total_width = 0; - for s in &mut sized { - let available_width = max_width - .filter(|_| s.priority.is_some()) - .map(|w| w.saturating_sub(total_width)); - s.size = segments[s.idx] - .widget - .size(widthdb, available_width, max_height) - .await; - if let Some(available_width) = available_width { - s.size.width = s.size.width.min(available_width); - } - total_width += s.size.width; - } - - sized -} - -async fn sizes_vert( - segments: &[Segment], - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, -) -> Vec { - let mut sized = segments - .iter() - .enumerate() - .map(|(i, s)| SizedSegment::new(i, s)) - .collect::>(); - sized.sort_by_key(|s| s.priority); - - let mut total_height = 0; - for s in &mut sized { - let available_height = max_height - .filter(|_| s.priority.is_some()) - .map(|w| w.saturating_sub(total_height)); - s.size = segments[s.idx] - .widget - .size(widthdb, max_width, available_height) - .await; - if let Some(available_height) = available_height { - s.size.height = s.size.height.min(available_height); - } - total_height += s.size.height; - } - - sized -} - -fn expand_horiz(segments: &mut [SizedSegment], available_width: u16) { - if !segments.iter().any(|s| s.expanding) { - return; - } - - // Interestingly, rustc needs this type annotation while rust-analyzer - // manages to derive the correct type in an inlay hint. - let current_width = segments.iter().map(|s| s.size.width).sum::(); - if current_width < available_width { - let mut remaining_width = available_width - current_width; - while remaining_width > 0 { - for segment in segments.iter_mut() { - if segment.expanding { - if remaining_width > 0 { - segment.size.width += 1; - remaining_width -= 1; - } else { - break; - } - } - } - } - } -} - -fn expand_vert(segments: &mut [SizedSegment], available_height: u16) { - if !segments.iter().any(|s| s.expanding) { - return; - } - - // Interestingly, rustc needs this type annotation while rust-analyzer - // manages to derive the correct type in an inlay hint. - let current_height = segments.iter().map(|s| s.size.height).sum::(); - if current_height < available_height { - let mut remaining_height = available_height - current_height; - while remaining_height > 0 { - for segment in segments.iter_mut() { - if segment.expanding { - if remaining_height > 0 { - segment.size.height += 1; - remaining_height -= 1; - } else { - break; - } - } - } - } - } -} - -/// Place multiple widgets next to each other horizontally. -pub struct HJoin { - segments: Vec, -} - -impl HJoin { - pub fn new(segments: Vec) -> Self { - Self { segments } - } -} - -#[async_trait] -impl Widget for HJoin { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - let sizes = sizes_horiz(&self.segments, widthdb, max_width, max_height).await; - let width = sizes.iter().map(|s| s.size.width).sum::(); - let height = sizes.iter().map(|s| s.size.height).max().unwrap_or(0); - Size::new(width, height) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - - let mut sizes = sizes_horiz( - &self.segments, - frame.widthdb(), - Some(size.width), - Some(size.height), - ) - .await; - expand_horiz(&mut sizes, size.width); - - sizes.sort_by_key(|s| s.idx); - let mut x = 0; - for (segment, sized) in self.segments.into_iter().zip(sizes.into_iter()) { - frame.push(Pos::new(x, 0), Size::new(sized.size.width, size.height)); - segment.widget.render(frame).await; - frame.pop(); - - x += sized.size.width as i32; - } - } -} - -/// Place multiple widgets next to each other vertically. -pub struct VJoin { - segments: Vec, -} - -impl VJoin { - pub fn new(segments: Vec) -> Self { - Self { segments } - } -} - -#[async_trait] -impl Widget for VJoin { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - let sizes = sizes_vert(&self.segments, widthdb, max_width, max_height).await; - let width = sizes.iter().map(|s| s.size.width).max().unwrap_or(0); - let height = sizes.iter().map(|s| s.size.height).sum::(); - Size::new(width, height) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - - let mut sizes = sizes_vert( - &self.segments, - frame.widthdb(), - Some(size.width), - Some(size.height), - ) - .await; - expand_vert(&mut sizes, size.height); - - sizes.sort_by_key(|s| s.idx); - let mut y = 0; - for (segment, sized) in self.segments.into_iter().zip(sizes.into_iter()) { - frame.push(Pos::new(0, y), Size::new(size.width, sized.size.height)); - segment.widget.render(frame).await; - frame.pop(); - - y += sized.size.height as i32; - } - } -} diff --git a/src/ui/widgets/layer.rs b/src/ui/widgets/layer.rs deleted file mode 100644 index fe0d983..0000000 --- a/src/ui/widgets/layer.rs +++ /dev/null @@ -1,38 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Size, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Layer { - layers: Vec, -} - -impl Layer { - pub fn new(layers: Vec) -> Self { - Self { layers } - } -} - -#[async_trait] -impl Widget for Layer { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - let mut max_size = Size::ZERO; - for layer in &self.layers { - let size = layer.size(widthdb, max_width, max_height).await; - max_size.width = max_size.width.max(size.width); - max_size.height = max_size.height.max(size.height); - } - max_size - } - - async fn render(self: Box, frame: &mut Frame) { - for layer in self.layers { - layer.render(frame).await; - } - } -} diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs deleted file mode 100644 index fc148a0..0000000 --- a/src/ui/widgets/list.rs +++ /dev/null @@ -1,395 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use parking_lot::Mutex; -use toss::{Frame, Pos, Size, WidthDb}; - -use super::{BoxedWidget, Widget}; - -/////////// -// State // -/////////// - -#[derive(Debug, Clone)] -struct Cursor { - /// Id of the element the cursor is pointing to. - /// - /// If the rows change (e.g. reorder) but there is still a row with this id, - /// the cursor is moved to this row. - id: Id, - /// Index of the row the cursor is pointing to. - /// - /// If the rows change and there is no longer a row with the cursor's id, - /// the cursor is moved up or down to the next selectable row. This way, it - /// stays close to its previous position. - idx: usize, -} - -impl Cursor { - pub fn new(id: Id, idx: usize) -> Self { - Self { id, idx } - } -} - -#[derive(Debug)] -struct InnerListState { - rows: Vec>, - - /// Offset of the first line visible on the screen. - offset: usize, - - cursor: Option>, - make_cursor_visible: bool, -} - -impl InnerListState { - fn new() -> Self { - Self { - rows: vec![], - offset: 0, - cursor: None, - make_cursor_visible: true, - } - } -} - -impl InnerListState { - fn first_selectable(&self) -> Option> { - self.rows - .iter() - .enumerate() - .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) - } - - fn last_selectable(&self) -> Option> { - self.rows - .iter() - .enumerate() - .rev() - .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) - } - - fn selectable_at_or_before_index(&self, i: usize) -> Option> { - self.rows - .iter() - .enumerate() - .take(i + 1) - .rev() - .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) - } - - fn selectable_at_or_after_index(&self, i: usize) -> Option> { - self.rows - .iter() - .enumerate() - .skip(i) - .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) - } - - fn selectable_before_index(&self, i: usize) -> Option> { - self.rows - .iter() - .enumerate() - .take(i) - .rev() - .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) - } - - fn selectable_after_index(&self, i: usize) -> Option> { - self.rows - .iter() - .enumerate() - .skip(i + 1) - .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) - } - - fn scroll_so_cursor_is_visible(&mut self, height: usize) { - if height == 0 { - // Cursor can't be visible because nothing is visible - return; - } - - if let Some(cursor) = &self.cursor { - // As long as height > 0, min <= max is true - let min = (cursor.idx + 1).saturating_sub(height); - let max = cursor.idx; - self.offset = self.offset.clamp(min, max); - } - } - - fn move_cursor_to_make_it_visible(&mut self, height: usize) { - if let Some(cursor) = &self.cursor { - let min_idx = self.offset; - let max_idx = self.offset.saturating_add(height).saturating_sub(1); - - let new_cursor = if cursor.idx < min_idx { - self.selectable_at_or_after_index(min_idx) - } else if cursor.idx > max_idx { - self.selectable_at_or_before_index(max_idx) - } else { - return; - }; - - if let Some(new_cursor) = new_cursor { - self.cursor = Some(new_cursor); - } - } - } - - fn clamp_scrolling(&mut self, height: usize) { - let min = 0; - let max = self.rows.len().saturating_sub(height); - self.offset = self.offset.clamp(min, max); - } -} - -impl InnerListState { - fn selectable_of_id(&self, id: &Id) -> Option> { - self.rows.iter().enumerate().find_map(|(i, r)| match r { - Some(rid) if rid == id => Some(Cursor::new(id.clone(), i)), - _ => None, - }) - } - - fn fix_cursor(&mut self) { - self.cursor = if let Some(cursor) = &self.cursor { - self.selectable_of_id(&cursor.id) - .or_else(|| self.selectable_at_or_before_index(cursor.idx)) - .or_else(|| self.selectable_at_or_after_index(cursor.idx)) - } else { - self.first_selectable() - } - } - - /// Bring the list into a state consistent with the current rows and height. - fn stabilize(&mut self, rows: &[Row], height: usize) { - self.rows = rows.iter().map(|r| r.id().cloned()).collect(); - - self.fix_cursor(); - if self.make_cursor_visible { - self.scroll_so_cursor_is_visible(height); - self.clamp_scrolling(height); - } else { - self.clamp_scrolling(height); - self.move_cursor_to_make_it_visible(height); - } - self.make_cursor_visible = true; - } -} - -pub struct ListState(Arc>>); - -impl ListState { - pub fn new() -> Self { - Self(Arc::new(Mutex::new(InnerListState::new()))) - } - - pub fn widget(&self) -> List { - List::new(self.0.clone()) - } - - pub fn scroll_up(&mut self, amount: usize) { - let mut guard = self.0.lock(); - guard.offset = guard.offset.saturating_sub(amount); - guard.make_cursor_visible = false; - } - - pub fn scroll_down(&mut self, amount: usize) { - let mut guard = self.0.lock(); - guard.offset = guard.offset.saturating_add(amount); - guard.make_cursor_visible = false; - } -} - -impl ListState { - pub fn cursor(&self) -> Option { - self.0.lock().cursor.as_ref().map(|c| c.id.clone()) - } - - pub fn move_cursor_up(&mut self) { - let mut guard = self.0.lock(); - if let Some(cursor) = &guard.cursor { - if let Some(new_cursor) = guard.selectable_before_index(cursor.idx) { - guard.cursor = Some(new_cursor); - } - } - guard.make_cursor_visible = true; - } - - pub fn move_cursor_down(&mut self) { - let mut guard = self.0.lock(); - if let Some(cursor) = &guard.cursor { - if let Some(new_cursor) = guard.selectable_after_index(cursor.idx) { - guard.cursor = Some(new_cursor); - } - } - guard.make_cursor_visible = true; - } - - pub fn move_cursor_to_top(&mut self) { - let mut guard = self.0.lock(); - if let Some(new_cursor) = guard.first_selectable() { - guard.cursor = Some(new_cursor); - } - guard.make_cursor_visible = true; - } - - pub fn move_cursor_to_bottom(&mut self) { - let mut guard = self.0.lock(); - if let Some(new_cursor) = guard.last_selectable() { - guard.cursor = Some(new_cursor); - } - guard.make_cursor_visible = true; - } -} - -//////////// -// Widget // -//////////// - -enum Row { - Unselectable { - normal: BoxedWidget, - }, - Selectable { - id: Id, - normal: BoxedWidget, - selected: BoxedWidget, - }, -} - -impl Row { - fn id(&self) -> Option<&Id> { - match self { - Self::Unselectable { .. } => None, - Self::Selectable { id, .. } => Some(id), - } - } - - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - match self { - Self::Unselectable { normal } => normal.size(widthdb, max_width, max_height).await, - Self::Selectable { - normal, selected, .. - } => { - let normal_size = normal.size(widthdb, max_width, max_height).await; - let selected_size = selected.size(widthdb, max_width, max_height).await; - Size::new( - normal_size.width.max(selected_size.width), - normal_size.height.max(selected_size.height), - ) - } - } - } -} - -pub struct List { - state: Arc>>, - rows: Vec>, - focus: bool, -} - -impl List { - fn new(state: Arc>>) -> Self { - Self { - state, - rows: vec![], - focus: false, - } - } - - pub fn focus(mut self, focus: bool) -> Self { - self.focus = focus; - self - } - - pub fn is_empty(&self) -> bool { - self.rows.is_empty() - } - - pub fn add_unsel>(&mut self, normal: W) { - self.rows.push(Row::Unselectable { - normal: normal.into(), - }); - } - - pub fn add_sel(&mut self, id: Id, normal: W1, selected: W2) - where - W1: Into, - W2: Into, - { - self.rows.push(Row::Selectable { - id, - normal: normal.into(), - selected: selected.into(), - }); - } -} - -#[async_trait] -impl Widget for List { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - _max_height: Option, - ) -> Size { - let mut width = 0; - for row in &self.rows { - let size = row.size(widthdb, max_width, Some(1)).await; - width = width.max(size.width); - } - let height = self.rows.len(); - Size::new(width, height as u16) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - - // Guard acquisition and dropping must be inside its own block or the - // compiler complains that "future created by async block is not - // `Send`", pointing to the function body. - // - // I assume this is because I'm using the parking lot mutex whose guard - // is not Send, and even though I was explicitly dropping it with - // drop(), rustc couldn't figure this out without some help. - let (offset, cursor) = { - let mut guard = self.state.lock(); - guard.stabilize(&self.rows, size.height.into()); - (guard.offset as i32, guard.cursor.clone()) - }; - - let row_size = Size::new(size.width, 1); - for (i, row) in self.rows.into_iter().enumerate() { - let dy = i as i32 - offset; - if dy < 0 || dy >= size.height as i32 { - continue; - } - - frame.push(Pos::new(0, dy), row_size); - match row { - Row::Unselectable { normal } => normal.render(frame).await, - Row::Selectable { - id, - normal, - selected, - } => { - let focusing = self.focus - && if let Some(cursor) = &cursor { - cursor.id == id - } else { - false - }; - let widget = if focusing { selected } else { normal }; - widget.render(frame).await; - } - } - frame.pop(); - } - } -} diff --git a/src/ui/widgets/padding.rs b/src/ui/widgets/padding.rs deleted file mode 100644 index e5b45cd..0000000 --- a/src/ui/widgets/padding.rs +++ /dev/null @@ -1,103 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Padding { - inner: BoxedWidget, - stretch: bool, - left: u16, - right: u16, - top: u16, - bottom: u16, -} - -impl Padding { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - stretch: false, - left: 0, - right: 0, - top: 0, - bottom: 0, - } - } - - /// Whether the inner widget should be stretched to fill the additional - /// space. - pub fn stretch(mut self, active: bool) -> Self { - self.stretch = active; - self - } - - pub fn left(mut self, amount: u16) -> Self { - self.left = amount; - self - } - - pub fn right(mut self, amount: u16) -> Self { - self.right = amount; - self - } - - pub fn horizontal(self, amount: u16) -> Self { - self.left(amount).right(amount) - } - - pub fn top(mut self, amount: u16) -> Self { - self.top = amount; - self - } - - pub fn bottom(mut self, amount: u16) -> Self { - self.bottom = amount; - self - } - - pub fn vertical(self, amount: u16) -> Self { - self.top(amount).bottom(amount) - } - - pub fn all(self, amount: u16) -> Self { - self.horizontal(amount).vertical(amount) - } -} - -#[async_trait] -impl Widget for Padding { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - let horizontal = self.left + self.right; - let vertical = self.top + self.bottom; - - let max_width = max_width.map(|w| w.saturating_sub(horizontal)); - let max_height = max_height.map(|h| h.saturating_sub(vertical)); - - let size = self.inner.size(widthdb, max_width, max_height).await; - - size + Size::new(horizontal, vertical) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - - let inner_pos = Pos::new(self.left.into(), self.top.into()); - let inner_size = if self.stretch { - size - } else { - Size::new( - size.width.saturating_sub(self.left + self.right), - size.height.saturating_sub(self.top + self.bottom), - ) - }; - - frame.push(inner_pos, inner_size); - self.inner.render(frame).await; - frame.pop(); - } -} diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs deleted file mode 100644 index b7ec0fe..0000000 --- a/src/ui/widgets/popup.rs +++ /dev/null @@ -1,74 +0,0 @@ -use toss::{Style, Styled}; - -use super::background::Background; -use super::border::Border; -use super::float::Float; -use super::layer::Layer; -use super::padding::Padding; -use super::text::Text; -use super::BoxedWidget; - -pub struct Popup { - inner: BoxedWidget, - inner_padding: bool, - title: Option, - border_style: Style, - bg_style: Style, -} - -impl Popup { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - inner_padding: true, - title: None, - border_style: Style::new(), - bg_style: Style::new().opaque(), - } - } - - pub fn inner_padding(mut self, active: bool) -> Self { - self.inner_padding = active; - self - } - - pub fn title>(mut self, title: S) -> Self { - self.title = Some(title.into()); - self - } - - pub fn border(mut self, style: Style) -> Self { - self.border_style = style; - self - } - - pub fn background(mut self, style: Style) -> Self { - self.bg_style = style; - self - } - - pub fn build(self) -> BoxedWidget { - let inner = if self.inner_padding { - Padding::new(self.inner).horizontal(1).into() - } else { - self.inner - }; - let window = - Border::new(Background::new(inner).style(self.bg_style)).style(self.border_style); - - let widget: BoxedWidget = if let Some(title) = self.title { - let title = Float::new( - Padding::new( - Background::new(Padding::new(Text::new(title)).horizontal(1)) - .style(self.border_style), - ) - .horizontal(2), - ); - Layer::new(vec![window.into(), title.into()]).into() - } else { - window.into() - }; - - Float::new(widget).vertical(0.5).horizontal(0.5).into() - } -} diff --git a/src/ui/widgets/resize.rs b/src/ui/widgets/resize.rs deleted file mode 100644 index b2edf22..0000000 --- a/src/ui/widgets/resize.rs +++ /dev/null @@ -1,86 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Size, WidthDb}; - -use super::{BoxedWidget, Widget}; - -pub struct Resize { - inner: BoxedWidget, - min_width: Option, - min_height: Option, - max_width: Option, - max_height: Option, -} - -impl Resize { - pub fn new>(inner: W) -> Self { - Self { - inner: inner.into(), - min_width: None, - min_height: None, - max_width: None, - max_height: None, - } - } - - pub fn min_width(mut self, amount: u16) -> Self { - self.min_width = Some(amount); - self - } - - pub fn max_width(mut self, amount: u16) -> Self { - self.max_width = Some(amount); - self - } - - pub fn min_height(mut self, amount: u16) -> Self { - self.min_height = Some(amount); - self - } - - pub fn max_height(mut self, amount: u16) -> Self { - self.max_height = Some(amount); - self - } -} - -#[async_trait] -impl Widget for Resize { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - max_height: Option, - ) -> Size { - let max_width = match (max_width, self.max_width) { - (None, None) => None, - (Some(w), None) => Some(w), - (None, Some(sw)) => Some(sw), - (Some(w), Some(sw)) => Some(w.min(sw)), - }; - - let max_height = match (max_height, self.max_height) { - (None, None) => None, - (Some(h), None) => Some(h), - (None, Some(sh)) => Some(sh), - (Some(h), Some(sh)) => Some(h.min(sh)), - }; - - let size = self.inner.size(widthdb, max_width, max_height).await; - - let width = match self.min_width { - Some(min_width) => size.width.max(min_width), - None => size.width, - }; - - let height = match self.min_height { - Some(min_height) => size.height.max(min_height), - None => size.height, - }; - - Size::new(width, height) - } - - async fn render(self: Box, frame: &mut Frame) { - self.inner.render(frame).await; - } -} diff --git a/src/ui/widgets/rules.rs b/src/ui/widgets/rules.rs deleted file mode 100644 index eaff35f..0000000 --- a/src/ui/widgets/rules.rs +++ /dev/null @@ -1,46 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, WidthDb}; - -use super::Widget; - -pub struct HRule; - -#[async_trait] -impl Widget for HRule { - async fn size( - &self, - _widthdb: &mut WidthDb, - _max_width: Option, - _max_height: Option, - ) -> Size { - Size::new(0, 1) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - for x in 0..size.width as i32 { - frame.write(Pos::new(x, 0), "─"); - } - } -} - -pub struct VRule; - -#[async_trait] -impl Widget for VRule { - async fn size( - &self, - _widthdb: &mut WidthDb, - _max_width: Option, - _max_height: Option, - ) -> Size { - Size::new(1, 0) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - for y in 0..size.height as i32 { - frame.write(Pos::new(0, y), "│"); - } - } -} diff --git a/src/ui/widgets/text.rs b/src/ui/widgets/text.rs deleted file mode 100644 index 3ce1dbe..0000000 --- a/src/ui/widgets/text.rs +++ /dev/null @@ -1,65 +0,0 @@ -use async_trait::async_trait; -use toss::{Frame, Pos, Size, Styled, WidthDb}; - -use super::Widget; - -pub struct Text { - styled: Styled, - wrap: bool, -} - -impl Text { - pub fn new>(styled: S) -> Self { - Self { - styled: styled.into(), - wrap: false, - } - } - - pub fn wrap(mut self, active: bool) -> Self { - // TODO Re-think and check what behaviour this setting should entail - self.wrap = active; - self - } - - fn wrapped(&self, widthdb: &mut WidthDb, max_width: Option) -> Vec { - let max_width = if self.wrap { - max_width.map(|w| w as usize).unwrap_or(usize::MAX) - } else { - usize::MAX - }; - - let indices = widthdb.wrap(self.styled.text(), max_width); - self.styled.clone().split_at_indices(&indices) - } -} - -#[async_trait] -impl Widget for Text { - async fn size( - &self, - widthdb: &mut WidthDb, - max_width: Option, - _max_height: Option, - ) -> Size { - let lines = self.wrapped(widthdb, max_width); - let min_width = lines - .iter() - .map(|l| widthdb.width(l.text().trim_end())) - .max() - .unwrap_or(0); - let min_height = lines.len(); - Size::new(min_width as u16, min_height as u16) - } - - async fn render(self: Box, frame: &mut Frame) { - let size = frame.size(); - for (i, line) in self - .wrapped(frame.widthdb(), Some(size.width)) - .into_iter() - .enumerate() - { - frame.write(Pos::new(0, i as i32), line); - } - } -}